How does an HStack divide a width that is too small, and what do layoutPriority and fixedSize change?

8.12.1 · L3 · Interview

Predict first

What widths do the two children get in a 200-point row?

@MainActor func run(_ label: String, _ v: some View) {
    frames = [:]
    _ = show(Root(content: v.frame(width: 200, alignment: .leading)))
    print(label, f("a"), "|", f("b"))
}
let short = Text("Short").lineLimit(1)
let long = Text("A considerably longer title here").lineLimit(1)
run("plain:", HStack(spacing: 0) { short.measure("a"); long.measure("b") })
run("long priority 1:", HStack(spacing: 0) { short.measure("a"); long.layoutPriority(1).measure("b") })
run("long fixedSize:", HStack(spacing: 0) { short.measure("a"); long.fixedSize().measure("b") })
run("color + 150:", HStack(spacing: 0) { Color.red.measure("a"); Color.blue.frame(width: 150).measure("b") })
  • plain 42/153, priority 2/198, fixedSize 0/237, colour 50/150
  • plain 100/100 and the others the same
  • priority changes nothing

Mental model. Seating at a crowded table: rigid guests are seated first, flexible ones fill the gaps, and a VIP (priority) is seated before anyone else even if that squeezes a friend into a corner.

The stack asks each child what size it would take for the proposal, sorts children by flexibility (how much their size can vary) and offers space to the least flexible first. In the check, in a 200-point row, a short Text and a long one with lineLimit(1) got 42 and 153: the short one is rigid at its natural width and the long one is squeezed. Giving the long text priority 1 reverses it: it takes 198 and the short label collapses to 2 points (an ellipsis), because higher priority is offered space first. fixedSize() makes a view take its ideal size and ignore the proposal: the long text reports 237 and overflows, leaving the neighbour 0. With a flexible Color next to a 150-point frame the colour simply takes the remainder, 50.

So when text is truncated or a label vanishes, do not reach for frames: decide who is allowed to shrink. Mark the important view with layoutPriority, let the secondary one truncate or wrap, and use fixedSize(horizontal: false, vertical: true) to let text grow in height instead of truncating. Ties in priority are resolved by flexibility, then by order. Avoid fixed widths for text (Dynamic Type breaks them), and use ViewThatFits or a different layout when the space is genuinely too small. Exact widths of text depend on the font and Dynamic Type, so treat the numbers as examples measured with the system font at the default size.

Space goes to rigid and high-priority children first.

Common traps

  • Using fixedSize() on long text in a row. It overflows and crushes its neighbours.
  • Fixing widths to stop truncation. It breaks at larger Dynamic Type sizes.

They might follow up with

  • How do you let text wrap instead of truncate? Remove lineLimit(1) and use fixedSize(horizontal: false, vertical: true).
  • Who wins between priority and flexibility? Priority first.

Related questions

  • 8.5.1 How does SwiftUI’s layout negotiation work, and why do frame and padding order change the result?
  • 8.12.2 How do alignment guides and custom alignments work, and why does moving one view’s guide move its siblings?
  • 8.5.3 How does the Layout protocol work, and when is a custom layout the right tool?

How do alignment guides and custom alignments work, and why does… →