Course outline

iOS / Swift / Swift values and SwiftUI ownership / Lesson 4

SwiftUI layout and stable lists

Reason about proposed size, modifier order, Dynamic Type, and row identity instead of fixing one screenshot.

About 5 min readContent reviewed
On this page

Lesson goals

  • Layout is a negotiation between container and child
  • Use semantic spacing and flexible composition
  • Read modifier order as nested structure

Layout is a negotiation between container and child

SwiftUI containers propose sizes, children choose sizes, and containers place the results. A frame modifier introduces another layout participant rather than changing an underlying view’s intrinsic nature. This explains why modifier order matters and why a fixed frame can clip content without making the text shorter. Begin a layout diagnosis with the available space and the content’s minimum useful size. Test narrow widths, landscape, and large Dynamic Type rather than assuming a device label determines the usable region. Safe areas, split views, and the keyboard can change that region during a single session.

Use semantic spacing and flexible composition

HStack and VStack express horizontal and vertical relationships. Spacer takes available room according to the surrounding layout, while layoutPriority can influence how competing children receive space. These mechanisms do not decide which information is essential. A secondary timestamp may truncate while a primary title should wrap; that is a product decision. Prefer leading and trailing alignment for reading-direction-aware structure. Avoid fixed heights for text-heavy cards unless the content contract truly bounds them, because translations and accessibility sizes can require more vertical space.

Read modifier order as nested structure

The first fragment creates two visually different backgrounds by changing modifier order. Padding applied before the background becomes part of the colored region; padding after it sits outside that region. The example is intentionally simple so the difference can be predicted without a screenshot. The same reasoning applies to clipping, overlays, frames, and hit areas. When an interactive target feels smaller than its visual card, inspect which layer owns the gesture and content shape. Do not assume a background alone automatically creates the desired semantic control.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
struct PaddingDemo: View {
    var body: some View {
        VStack {
            Text("Inside").padding(12).background(.teal)
            Text("Outside").background(.teal).padding(12)
        }
    }
}
Expected output
Inside has a padded teal region; Outside has teal behind its text with outer padding.

Requires SwiftUI. The example predicts modifier composition rather than a measured pixel size or tested screenshot.

Identify rows by domain identity

The second fragment uses Identifiable records with stable string identifiers. List can associate row identity with each record while its position changes. Do not regenerate identifiers during body evaluation or use a mutable display label as identity when labels can collide or change. A row’s local draft can otherwise migrate or reset during sorting. Keep selection by identifier rather than an index into one array snapshot. If an item disappears, decide whether selection clears or becomes an unavailable state instead of reading an old index.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
struct ReadingItem: Identifiable {
    let id: String
    let title: String
}
struct ReadingList: View {
    let items = [ReadingItem(id: "b-7", title: "Maps"),
                 ReadingItem(id: "b-9", title: "Rivers")]
    var body: some View { List(items) { item in Text(item.title) } }
}
Expected output
The list displays Maps and Rivers with identities b-7 and b-9.

Stable identifiers represent items, not slots. This fragment does not test virtualization, navigation, or device rendering.

Choose scrolling composition deliberately

List provides platform-aware list behavior, while ScrollView with a suitable stack gives more custom composition. Lazy stacks defer construction for appropriate scrolling content but do not make every row operation inexpensive. Avoid nesting scrollable containers simply to fix clipping without deciding which surface should own scrolling. Expensive sorting, image decoding, or network work inside each row can remain a problem regardless of lazy construction. Keep row data ready under a model owner and profile representative scrolling before replacing standard containers with more complex custom machinery.

Test layout with changing content and identity

Review empty data, many rows, long Arabic labels, mixed-direction identifiers, accessibility text sizes, and a keyboard-visible form above results. Then edit a stateful row, reorder the collection, and verify its draft still belongs to the same identifier. A static preview of two short English rows cannot reveal these failures. Inspect focus order and accessibility labels as well as visible clipping. Keep any screenshot comparison tied to a known simulator configuration and font environment; a changed image must be reviewed rather than blindly accepted as a new baseline.

Common errors and fixes

Padding is not included in the colored region

Why it happens
The background modifier appears before padding.
How to fix it
Move padding before background when the intended region includes it.
Prevent it next time
Read modifier chains as nested layout layers.

A selected row changes after insertion

Why it happens
Selection tracks an index rather than item identity.
How to fix it
Store the stable record identifier and resolve it against current data.
Prevent it next time
Test insertion and deletion around the selected row.

Large text clips a card

Why it happens
Fixed geometry assumed one string length and font size.
How to fix it
Allow growth, wrapping, or a responsive arrangement.
Prevent it next time
Include Dynamic Type and Arabic in layout review.

Check your understanding

1. Why is a newly generated UUID unsuitable as a stable row ID during each body evaluation?

Show worked solution

It tells SwiftUI each description is a new item, undermining state continuity and diffing. Generate identity when the domain record is created or use its existing stable key. Reordering should change position while preserving the same item identity.

2. Should truncation be the default fix for every translated label?

Show worked solution

No. Decide which information is essential. Allow a primary label to wrap or change the arrangement when needed; reserve truncation for cases where meaning remains recoverable. Test the action and accessibility label so the shortened visual presentation does not hide the task.

Key takeaways

  • Layout is a negotiation between container and child
  • Test layout with changing content and identity

Official references