Overview
If you’ve built websites with CSS, you’re familiar with the box model — the idea that every element is a rectangular box with content, padding, border, and margin. SwiftUI has its own layout system that achieves similar results with a fundamentally different approach. Understanding how SwiftUI thinks about layout will make your UI work much more predictable and much less frustrating.
The Layout Negotiation Loop
Every time SwiftUI draws your interface, it runs a three-step conversation between each parent view and its children:
- Offer: The parent offers a proposed size to the child — essentially saying “you have this much space available.”
- Decide: The child looks at the proposal and decides how much space it actually wants to take.
- Report: The child tells the parent its actual size. The parent then uses that size to position the child.
This is the key insight: in SwiftUI, views size themselves. The parent can only suggest; the child decides. This is very different from CSS, where you typically set dimensions on elements directly.
A practical consequence: Text always takes only as much space as the text it contains. A Rectangle with no size constraints will expand to fill all available space. Understanding which views “hug their content” vs. which ones “fill available space” is the foundation of SwiftUI layout.
Layout Containers: HStack, VStack, ZStack
SwiftUI’s three stacks are the primary layout containers, and each maps roughly to a CSS layout concept:
- VStack — arranges children vertically. Analogous to
display: flex; flex-direction: columnin CSS. - HStack — arranges children horizontally. Analogous to
display: flex; flex-direction: row. - ZStack — layers children on top of each other. Analogous to
position: absolutestacking in CSS.
All three stacks accept an optional spacing: parameter that controls the gap between children — similar to CSS gap in flexbox.
VStack(spacing: 16) {
Text("Top")
Text("Middle")
Text("Bottom")
}
Padding: Space Around Content
In CSS, padding is a property that adds space between an element’s content and its border. In SwiftUI, .padding() is a modifier that works the same way — it adds space around a view’s content on the outside.
Text("Hello")
.padding() // default padding on all sides (~16pts)
.padding(.horizontal, 20) // 20pts left and right only
.padding(.bottom, 8) // 8pts at the bottom only
You can stack multiple padding modifiers on the same view. Each one wraps around the previous result, adding more space layer by layer.
Frame: Setting Explicit Dimensions
The .frame() modifier is the closest SwiftUI equivalent to setting width and height in CSS. It constrains (or expands) a view to specific dimensions.
Rectangle()
.frame(width: 100, height: 50) // fixed size
Text("Stretch me")
.frame(maxWidth: .infinity) // fill all available width
Text("Tall area")
.frame(height: 200, alignment: .top) // fixed height, content aligned top
The maxWidth: .infinity pattern is especially useful — it’s the SwiftUI equivalent of width: 100% or flex: 1 in CSS. It tells the view to claim as much horizontal space as the parent offers.
Spacer: Flexible Empty Space
Spacer() is a view that expands to fill all available space along the stack’s axis. It’s the SwiftUI equivalent of a flex: 1 empty div in CSS flexbox. Use it to push other views to the edges of a container:
HStack {
Text("Left side")
Spacer()
Text("Right side")
}
In this example, Spacer() pushes “Left side” to the left edge and “Right side” to the right edge — just as you’d achieve with justify-content: space-between in CSS flexbox.
Alignment
Stacks accept an alignment: parameter to control how children line up perpendicular to the stack’s axis:
VStack(alignment: .leading) { ... } // left-align children (like text-align: left)
HStack(alignment: .bottom) { ... } // bottom-align children
The .frame() modifier also accepts an alignment: parameter, which controls where the view’s content sits within the frame — similar to CSS align-items and justify-content.
The Modifier Order Rule
In CSS, properties like background and border are attributes of a box — they don’t depend on order. In SwiftUI, modifier order matters. Each modifier wraps the previous view in a new layer, and that layering affects the result.
Compare these two examples:
// Example A: padding INSIDE the background
Text("Hello")
.padding()
.background(Color.blue) // background fills the padded area
// Example B: padding OUTSIDE the background
Text("Hello")
.background(Color.blue) // background hugs the text only
.padding() // space is added outside the blue area
In Example A, the blue background fills the padded area. In Example B, the blue background hugs the text, and the padding is transparent space around the blue box. This is the direct analog of CSS padding (inside the background) vs. margin (outside the background) — but controlled entirely by modifier order.
Putting It Together: A SwiftUI Card
Here’s how the CSS box model maps to SwiftUI in a real example:
Text("Course: NMIX 4030")
.font(.headline)
.padding(16) // like CSS padding
.frame(maxWidth: .infinity, // like CSS width: 100%
alignment: .leading)
.background(Color.white) // the "box" background
.cornerRadius(8) // like CSS border-radius
.shadow(radius: 4) // like CSS box-shadow
.padding(.horizontal, 20) // like CSS margin (outside the card)
Key Takeaways
- SwiftUI uses a parent-offers / child-decides layout model rather than direct dimension setting.
Textand similar views hug their content;Rectangle,Color, andSpacerfill available space.VStack,HStack, andZStackare the primary layout containers — roughly equivalent to CSS flexbox column, row, and absolute stacking..padding()adds space inside a background;.padding()applied after a background acts like CSS margin..frame(maxWidth: .infinity)is the SwiftUI equivalent ofwidth: 100%orflex: 1.- Modifier order determines visual output — the same modifiers in a different order produce a different result.
Your Tasks
Work through these three exercises in a single Xcode project to build intuition for SwiftUI’s layout system.
Task 1: Content Hugging vs. Space Filling
- Create a new SwiftUI view. Add a
Textview and aRectangleone above the other in aVStack. - Run the preview. Notice that the
Rectanglefills all remaining space. - Add
.frame(height: 100)to theRectangle. Now it takes a fixed size. - Now add
.frame(maxWidth: .infinity)to theText. Give it a background color. Notice it now stretches to fill the full width of the screen.
Task 2: Recreate the Box Model
Using only SwiftUI modifiers, recreate the CSS box model:
- Start with a
Textview containing a short label. - Add
.padding(16)to create inner spacing. - Add
.background(Color.white)to create the “box”. - Add
.cornerRadius(8)and.shadow(radius: 4)to polish it. - Finally, add another
.padding(.horizontal, 20)after the shadow to create outer margin. - Experiment: swap the order of
.padding()and.background()and observe what changes.
Task 3: Navigation Bar Layout
Build a simple top navigation bar using what you’ve learned:
- Create an
HStackcontaining a titleTexton the left and an SF Symbol button on the right. - Use
Spacer()to push the title left and the button right. - Give the entire HStack a background color and padding.
- Wrap the whole thing in a
VStackand push it to the top of the screen usingSpacer()at the bottom.