Tutorials › Launch Lab Notes › Chapter 22

Same Feature, Three Bodies

LabChapter 22 of the Launch Lab Notes24 minAugust 20, 2026Intermediate

Architecture arguments are usually tribal. This bench makes them countable.

Same feature: bump Harbor’s build by one. Three ways to hold the state. A body-run counter on each panel — the ch6 meter, reused.

Filmstrip: MV BODY climbs with shared chrome; MVVM and REDUCER DIGIT climb while CHROME stays at 1.

Midway — the split appears

After 1 bump (build 401):

BUMPS 1. MV BODY 2 chrome shares it. MVVM DIGIT 2 CHROME 1. REDUCER DIGIT 2 CHROME 1. BUILD 401 all.
PanelDigit / bodyChrome
MVBODY 2shares the same body
MVVMDIGIT 2CHROME 1
REDUCERDIGIT 2CHROME 1

Settled — five bumps

-mode settle stops at five. Build 405 everywhere.

After 5 bumps BUILD 405. MV BODY 6. MVVM DIGIT 6 CHROME 1. REDUCER DIGIT 6 CHROME 1.
PanelAfter 5 bumps
MVBODY 6 — READY chrome lives in that body
MVVMDIGIT 6, CHROME 1
REDUCERDIGIT 6, CHROME 1

MVVM and the reducer match on the meter. The difference between them is where the next change goes (a bump() method vs dispatch(.increment) through shipGateReduce), not how often SwiftUI redraws a silent label.

What each column is

MV (control). @State var build and the READY label sit in one body. Every +1 re-runs the whole panel — chrome has nowhere else to live.

MVVM. @Observable ShipGateVM with bump(). A digit child reads vm.build. A chrome child reads nothing. Observation invalidates only the digit.

Reducer. @Observable ShipGateStore holds ShipGateState. dispatch(.increment) runs a pure shipGateReduce. Same child split as MVVM — unidirectional write path, same body-run story.

The first draft lied

Putting bumps on the column’s @State made CHROME climb with DIGIT (both hit 6). The parent re-ran every tick and dragged chrome with it. The fix: an @Observable BumpTicker that only BumpBadge reads. Architecture cannot save you from a parent that invalidates everyone — ch6 again, in costume.

Mini-exercise

Add a CREW label in the MVVM chrome that reads vm.build. Watch CHROME start climbing with DIGIT. Remove the read; it freezes again.

Challenges

  1. Port the MV column to a UIKit UIViewController that mutates a model property — count viewDidLayoutSubviews instead of body runs.
  2. Add a fourth column: VIPER-style presenter that pushes build into the view via a protocol; meter the view body only.
  3. Make the reducer log every action to an on-screen tape (ch12 style) without bumping chrome.
  4. Replace @Observable MVVM with ObservableObject + @Published and compare chrome runs to ch6’s legacy row.
  5. Drive all three columns from one shared ticker without letting the bench parent own per-bump @State.

Key Points

Next up: images and memory — full-resolution vs downsampled thumbnails with a live memory readout.

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 21: Swap the ServiceCh 23: Full Res vs Thumbnails→
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