Tutorials › Launch Lab Notes › Chapter 19

What the Subviews Answered

LabChapter 19 of the Launch Lab Notes20 minAugust 20, 2026Intermediate

Chapter 7 asked what happens when a parent offers a width. This chapter asks who answers.

A custom Layout calls sizeThatFits on every chip, then places them. A plain HStack gets the same chips and never wraps — that is the control.

Filmstrip: offered width 300→210→150; FLOW gains rows while HSTACK stays one clipped row.

The answers (every offer)

A:64×36 · B:56×36 · C:72×36 · D:48×36 · E:96×36

Those are what each chip reports from sizeThatFits(.unspecified). The offer changes; the answers do not.

Wide — 300 pt → 2 rows

OFFERED 300. FLOW 2 rows. HSTACK 1. FLOW shows App v1.4 Build 418 on row 1, TestFlight on row 2. HSTACK clips TestFlight to Te…
CardObserved
OFFERED300 pt
FLOW2 rows
HSTACK1 row (clipped)

FLOW fits App · v1.4 · Build · 418, then wraps TestFlight. HSTACK keeps one row and clips Te….

Mid — 210 pt → still 2 rows, earlier wrap

OFFERED 210. FLOW 2 rows with App v1.4 Build on row 1, 418 TestFlight on row 2. HSTACK 1 clipped.
CardObserved
OFFERED210 pt
FLOW2 rows
HSTACK1 row

Row 1 stops after Build (192 pt of chips). 418 and TestFlight take row 2.

Narrow — 150 pt → 3 rows

OFFERED 150. FLOW 3 rows: App v1.4 / Build 418 / TestFlight. HSTACK 1 clipped after v1.4.
CardObserved
OFFERED150 pt
FLOW3 rows
HSTACK1 row
struct ShipFlowLayout: Layout {
  func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { … }
  func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { … }
}

Inside arrange, each subview answers with subview.sizeThatFits(.unspecified). If x + size.width would exceed proposal.width, the layout resets x and starts a new row.

HStack is not a failed FlowLayout

The rose panel is the control on purpose. Same chips, same offer, 1 row always, content clipped. Wrapping is not free — you write it in placeSubviews.

Sequel to ch7

Negotiation printed what candidates took from an offer. This bench prints what a Layout heard (ProposedViewSize) and what subviews answered (sizeThatFits) before placement.

Mini-exercise

Change chip E from 96 to 40. At offer 300, FLOW should drop to 1 row. Re-capture mentally: 64+56+72+48+40 + spacers fits under 300.

Challenges

  1. Add cache that stores the arrange result so sizeThatFits and placeSubviews share one pass.
  2. Honor a vertical proposal — if height is tight, stop placing and report truncated count on a card.
  3. Replace fixed chip widths with Text that sizes from content and photograph new answers.
  4. Put ShipFlowLayout beside LazyVGrid with two columns and compare row counts at 150 pt.
  5. XCUITest: after -mode narrow, assert text contains OFFERED / 150 and FLOW / 3.

Key Points

Next up: testing SwiftUI — a view model tested in isolation vs the same behaviour through XCUITest, reusing the ch8 harness.

SwiftUI
SwiftUI tutorials for building native app screens, layouts, navigation, and state-driven interfaces.
Swift
Swift fundamentals for app developers who want to understand the language behind real iOS and macOS apps.
Ship iOS
Shipping workflows for iOS apps.
📚 Go deeper with LIPAI WANG’s hands-on Udemy bootcampsBrowse all courses →
← Ch 18: Down the Tree, Up the TreeCh 20: What the Tests Catch→
SwiftUIUltimate SwiftUI SeriesSwiftUI tutorials for building native app screens, layouts, navigation, and state-driven interfaces.SwiftUltimate Swift SeriesSwift fundamentals for app developers who want to understand the language behind real iOS and macOS apps.Ship iOSShip iOS Apps SeriesShipping workflows for iOS apps: signing, TestFlight, App Store Connect, CI, and release hygiene.

Ship your apps faster

When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.

Try Simple App Shipper
5 free articles remainingSubscribe for unlimited access