iOS / Swift / Concurrency and data boundaries / Lesson 11
Versioned storage and SwiftData contexts
Distinguish serialization, tracked in-memory changes, persistence, and authoritative synchronization.
On this page
Lesson goals
- Name the guarantee of each storage layer
- Version a portable draft envelope
- Attach the intended model container
Name the guarantee of each storage layer
A JSON string, a modified model object, a successful local save, and a server-confirmed command are different milestones. Serialization only produces bytes; it does not guarantee those bytes survive process termination. SwiftData tracks persistent models through a container and context, while an application still chooses validation, save boundaries, migration, privacy, and synchronization policy. Treat a disposable response cache differently from an irreplaceable draft. Define expected lifetime, sensitivity, schema version, and recovery behavior before selecting a convenience storage API. A preferences mechanism is not automatically an appropriate place for credentials or transactional records.
Version a portable draft envelope
The first fragment decodes a versioned draft using Codable, then checks the schema number. A reader can distinguish unsupported content from an accepted draft instead of interpreting changed fields under an old assumption. A future migration should preserve meaning and validate its output, not merely rename keys until decoding succeeds. The fixture contains synthetic text only. No file or database is opened here. For important user work, an unsupported version should trigger a recovery or export decision rather than silent deletion hidden behind an empty screen.
import Foundation
struct DraftEnvelope: Codable {
let schema: Int
let title: String
}
enum DraftFailure: Error { case unsupportedSchema }
func readDraft(_ bytes: Data) throws -> DraftEnvelope {
let value = try JSONDecoder().decode(DraftEnvelope.self, from: bytes)
guard value.schema == 1 else { throw DraftFailure.unsupportedSchema }
return value
}
let bytes = Data(#"{"schema":1,"title":"Maps"}"#.utf8)
// try readDraft(bytes).title -> "Maps"
Schema 1 accepts Maps; another schema throws unsupportedSchema.
Decoding local bytes establishes format acceptance only, not filesystem durability or database migration.
Attach the intended model container
SwiftData needs a container that knows the model schema and storage configuration. A SwiftUI environment context comes from the attached container; do not assume any environment lookup proves the correct persistent store is configured. The second fragment declares a small model and explicitly requests a save under a supplied main-actor context. The caller must attach and inject the intended container and handle failures. This is an illustrative fragment only: it does not create or mutate a real store. A successful explicit save still does not establish a remote system accepted the same data.
import SwiftData
@Model final class SavedDraft {
var title: String
init(title: String) { self.title = title }
}
@MainActor
func insertDraft(title: String, context: ModelContext) throws {
let draft = SavedDraft(title: title)
context.insert(draft)
try context.save()
}
The supplied context tracks a new draft and attempts an explicit save.
Requires a compatible SwiftData host and a container including SavedDraft. No store was opened, saved, or queried during authoring.
Keep context and model lifetimes coherent
A persistent model belongs to a managed context and should not be passed arbitrarily across concurrency boundaries as an ordinary Sendable value. Use stable identifiers or immutable snapshots and resolve them in the receiving persistence scope under documented APIs. The environment main context is main-actor bound; background persistence requires an explicit supported isolation design. Do not fix isolation diagnostics with unchecked annotations. Also consider pending changes: rollback can affect more than one edit in a shared context, so Cancel for a form may be better implemented with an independent draft than by rolling back unrelated work.
Choose save and failure policy deliberately
Autosave behavior is not a substitute for defining when the product considers an edit accepted. If a command needs explicit confirmation, call the appropriate save boundary and handle its error before announcing success. A failed save can leave tracked in-memory changes that still appear in the UI; distinguish them from persisted state and provide retry or recovery. Schema evolution needs a reviewed migration plan and fixtures from supported prior versions. Never delete the store as the default response to a migration failure when it contains user work. Diagnose container, schema, and configuration identity first.
Test format and persistence boundaries separately
Pure fixture tests can establish envelope decoding and migration logic without touching a real database. An isolated in-memory SwiftData container in an authorized SDK host can then test schema registration, insertion, fetching, and save-error handling where controllable. It does not prove disk durability, process interruption recovery, or cloud synchronization. Keep those as distinct target integration tasks. No SwiftData operation was executed for this course. Validation of the public JSON lesson files cannot establish Apple persistence behavior, and the course deliberately does not claim that it can.
Common errors and fixes
A visible edit is reported as durably saved
- Why it happens
- Tracked in-memory change was confused with successful persistence.
- How to fix it
- Use an explicit acceptance boundary and handle save failures.
- Prevent it next time
- Test the unsaved and failed-save states.
A model is used through an unrelated context
- Why it happens
- Managed object ownership crossed persistence or actor scopes.
- How to fix it
- Pass identity or immutable values and resolve in the intended scope.
- Prevent it next time
- Document context ownership with repository APIs.
Migration recovery deletes valuable drafts
- Why it happens
- Store deletion was chosen before diagnosis and recovery policy.
- How to fix it
- Preserve data and inspect schema/container compatibility before a reviewed recovery.
- Prevent it next time
- Maintain prior-version fixtures and recovery paths.
Check your understanding
1. Does a successful Codable round trip prove a draft survives an app restart?
Show worked solution
No. It proves that the selected values can be encoded and decoded under that format. Storage durability, write completion, file protection, and restart recovery require separate evidence in the actual persistence host. Label each result by the boundary it exercises.
2. Why can rolling back a shared context be too broad for Cancel?
Show worked solution
The context may contain changes from other workflows. A rollback can discard those pending edits as well as the current formโs changes. Prefer an isolated editable draft or a deliberately scoped transaction design and verify that cancellation preserves unrelated work.
Key takeaways
- Name the guarantee of each storage layer
- Test format and persistence boundaries separately