Tutorials › Launch Lab Notes › Chapter 13

What Survives a Relaunch: Memory, Defaults, JSON, and SwiftData

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

Where did the note go?

You typed it. You saw it on screen. You killed the app. On the next launch three stores still had "Harbor build 418" and one showed a dash. That dash is not a bug in the demo — it is the control.

Seed run fills all four cards with Harbor build 418; the recording stays on the seed path while the relaunch still is captured as a separate process.

Four stores, one string

static let seededValue = "Harbor build 418"
StoreMechanism
MEMORY@State only
DEFAULTSUserDefaults.standard
JSONDocuments/lab-shipment-note.json via Codable
SWIFTDATA@Model + on-disk ModelContainer

Capture does this for real: one process with -mode seed, then a new process with -mode reload that only reads. No fake "clear memory" button.

SEED SEEDED: all four cards show Harbor build 418. MEMORY labelled LIVE; DEFAULTS, JSON, SWIFTDATA labelled SURVIVED.

Right after the write, everything agrees. MEMORY says LIVE. The durable three already say SURVIVED because the read-back came from disk, not from hope.

After kill

RELOAD AFTER KILL: MEMORY shows an em dash labelled GONE; DEFAULTS, JSON, and SWIFTDATA still show Harbor build 418 labelled SURVIVED.
StoreAfter relaunchLabel
MEMORY—GONE
DEFAULTSHarbor build 418SURVIVED
JSONHarbor build 418SURVIVED
SWIFTDATAHarbor build 418SURVIVED

Footer on that still: MODE RELOAD · AFTER KILL.

@State is not a database

It is a value tied to a view identity in the current process. Kill the process and there is nothing to restore. Shipping an editor that only mutates @State is how notes vanish when the user switches apps under memory pressure — the failure looks like "the app forgot," not like a crash.

How each survivor is written

UserDefaults — one key, one string:

UserDefaults.standard.set(text, forKey: Self.noteKey)
// …
defaultsValue = UserDefaults.standard.string(forKey: Self.noteKey) ?? "—"

Fine for small preferences. Wrong for large documents, and invisible when you forget the key name.

JSON file — the Saving Files pattern from SwiftUI Apprentice, narrowed to one note:

struct NoteFile: Codable { var text: String }
 
try JSONEncoder().encode(NoteFile(text: text))
  .write(to: jsonURL, options: .atomic)

Atomic write matters: a half-written file after a kill is how "JSON survived" becomes "JSON corrupted."

Instant seeds produce empty recordings

The first capture wrote all four stores in one frame and the motion file came back 0.06 s long — unusable for social export. The seed path now pauses between cards so the filmstrip has something to show. Same data, slower ceremony.

SwiftData — a one-field @Model in a file-backed container under Application Support:

@Model
final class PersistedNote {
  var text: String
  init(text: String) { self.text = text }
}

Same string came back. The point of this bench is not schema design — it is that choosing any durable store beats pretending @State would remember.

The control only looks wrong after relaunch

On the seeded still, MEMORY looks identical to the winners. That is the ch9 / ch10 / ch12 trap again: happy-path screenshots hide the lifetime bug. The money shot is always the second process.

Mini-exercise

In reloadAll(), restore memory from UserDefaults "to be helpful." Re-run -mode reload. MEMORY will falsely say LIVE. You just destroyed the control — put the blanking back.

Challenges

  1. Delete the JSON file in Finder between seed and reload; photograph JSON MISS while Defaults and SwiftData still survive.
  2. Switch the SwiftData ModelConfiguration to isStoredInMemoryOnly: true and show it joins MEMORY in the GONE column after kill.
  3. Write a second field (updatedAt) only to SwiftData and show the JSON card cannot display it without a format bump — motivate migrations (next queue item).
  4. Use ScenePhase.background to seed Defaults automatically and prove a background write still survives kill.
  5. XCUITest: launch seed, terminate via XCUIApplication().terminate(), relaunch reload, assert MEMORY accessibility value is the dash and SWIFTDATA contains 418.

Key Points

Next up: migrations — load a store written by an older schema; one build migrates, one crashes; show the actual error text.

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 12: Two Debounces, One TapeCh 14: The Schema Moved→
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