The layout system
SwiftUI layout is a three-step negotiation between parent and child — once you know the three steps, layout stops being guesswork.
By the end you will be able to
- State the three steps of SwiftUI's layout negotiation
- Predict how a stack divides space between its children
- Use frame, Spacer, padding and alignment deliberately rather than by trial and error
Text("Hi").frame(width: 200, height: 100) — what is the size of the Text itself?
The three steps
Every layout in SwiftUI is the same negotiation, repeated down the tree:
- The parent proposes a size to the child.
- The child chooses its own size — it may accept the proposal, take less, or take more.
- The parent places the child within its own bounds.
The crucial part is step 2: the child decides. A parent proposes; it cannot compel. That is the reverse of Auto Layout, where constraints are solved globally, and it is why "why is this view not filling the space?" is nearly always answered by "because the child chose not to".
How different views respond to a proposal
| View | Given a large proposal, it takes |
|---|---|
Text | exactly what it needs for its content |
Image | its natural size, unless .resizable() |
Color, Rectangle, shapes | all of it — shapes are greedy |
Spacer | all of it along the stack's axis |
VStack / HStack | only what its children need |
.frame(width:height:) | exactly what you specified |
The Text has a background only as wide as its letters. The Rectangle fills every available point of width, because shapes accept whatever they are offered.
Stacks divide space
A VStack proposes to its children in order of flexibility: the least flexible first. Inflexible children (like Text) take what they need; whatever is left is divided among the flexible ones.
This is why a Spacer pushes things apart: it is maximally flexible, so it absorbs all remaining space along the axis.
.frame makes a container
Because .frame creates a parent rather than resizing the child, these two are different:
Text("Hi").frame(width: 200).background(.blue) // blue fills 200pt
Text("Hi").background(.blue).frame(width: 200) // blue hugs the text
In the first, the background is applied to the 200-point frame. In the second, it is applied to the text, and the frame wraps the already-backgrounded text. Modifier order is layout order — the next lesson is entirely about this.
.frame(maxWidth: .infinity) is the one you will reach for constantly: it means "take as much width as offered", turning a modest view into a greedy one.
Alignment
Stacks take an alignment for the axis they do not control:
VStack(alignment: .leading) { … } // controls horizontal placement
HStack(alignment: .top) { … } // controls vertical placement
ZStack(alignment: .bottomTrailing) { … }ZStack layers
ZStack draws its children back to front — first child at the back:
Two views, identical except for where .padding() sits. Look at the preview and decide which rectangle is larger before you run it.
#Preview {
VStack(spacing: 12) {
Text("Padding inside")
.padding()
.background(.blue)
Text("Padding outside")
.background(.blue)
.padding()
}
}Your VStack sits as a small block in the middle of the screen instead of filling it. What is the most likely fix?
Build a header bar
Build a row with a title on the left, a Spacer, and a caption on the right — full width, padded, on a grey background.
Layer a badge
Use a ZStack to place a small orange circle at the top-trailing corner of a blue rounded rectangle.
Predict what Text("Hi").frame(width: 50).frame(width: 200).background(.blue) looks like — two frames stacked. Write your prediction, then build it in the playground and check.
You can now:
- State the three-step layout negotiation and say who decides the size
- Predict how a stack divides space and why
Spacerbehaves as it does - Use
.frame,maxWidth: .infinityand alignment on purpose - Layer views with
ZStack
Next up: modifier order — why the same modifiers in a different sequence produce a different view.