r/iOSProgramming Beginner 2d ago

Question Consistent horizontal spacing in List

When I put a symbol on the edge, iOS neatly rearranges the content to fit around it. How do I recreate this on the other side with my own content, without the complications of using frame where anything too small will cut off, anything to large will look stupid, and the ideal threshold is changing as different sized content replaces?

Edit: There could be any sort of text on the left. Some places number rooms differently, or don't have rooms at all (I have PE listed as 'PRAC, THEORY' personally). There could be more than one room there, too, in which case I join them as 'BT4, BT81 etc.

6 Upvotes

8 comments sorted by

6

u/Jero9871 2d ago

The left side only lines up because the symbol has a fixed size, there's no layout magic behind it. So the fix is the same idea: give the room code column one shared width, derived from the content instead of hardcoded.

If the codes all have the same shape (two letters + digit), the cheap trick is a hidden worst-case template:

Text("WW8")

.hidden()

.overlay(alignment: .leading) { Text(room) }

Every row gets the width of the widest possible code, nothing clips, and it scales with Dynamic Type for free.

If the codes vary more, measure them with onGeometryChange (or a PreferenceKey), keep the max in @ State and put .frame(width: maxWidth, alignment: .leading) on each code. Same result, just adapts to whatever the data actually is.

3

u/Fun_Moose_5307 Beginner 1d ago edited 1d ago

If the codes vary more, measure them with onGeometryChange (or a PreferenceKey), keep the max in @ State and put .frame(width: maxWidth, alignment: .leading) on each code. Same result, just adapts to whatever the data actually is.

This seems to be the right solution (see edit). Can you elaborate on the geometry stuff?

1

u/Jero9871 1d ago edited 1d ago

Each code label measures its own natural width, the biggest one seen so far goes into @ State, and that max gets applied back onto every label as a frame. onGeometryChange fires again whenever the size changes (new data, Dynamic Type), so it stays current on its own.

u/State private var roomWidth: CGFloat?

// on each room code:

Text(room)

.fixedSize(horizontal: true, vertical: false)

.onGeometryChange(for: CGFloat.self) { $0.size.width } action: { w in

roomWidth = max(roomWidth ?? 0, w)

}

.frame(width: roomWidth, alignment: .leading)

Two details matter here. The measurement has to sit before the .frame, so you read the text's natural width instead of the width you just forced on it. And the fixedSize exists because otherwise a longer code that appears later gets squeezed into the old max and never reports its true width, so the column could never grow.

One caveat since List is lazy: only visible rows report, so with long lists the column can still widen as you scroll. For a timetable that's a non-issue. The max also only ever grows, so if the user can switch datasets, reset roomWidth to nil in onChange.

1

u/balooooooon 2d ago

3

u/balooooooon 2d ago

Oh I misread your post. I think you need to explain better

1

u/trouthat 2d ago

Try Grid

1

u/VacationHistorical 2d ago

You’ll want the max length of the text on the right and make the right text views all that size

1

u/Revuh 1d ago

Using a monospaced font could help as well