Data architecture, source of truth, caching, offline sync, and conflict
In a local-first app, the user must see a successful local edit even when the network is unavailable. Durable device state drives the interface while synchronization reconciles it with server state.
Write locally and enqueue atomically
struct PendingOperation: Codable, Sendable { let operationID: UUID let noteID: UUID let baseRevision: Int let change: NoteChange}
protocol LocalNoteStore: Sendable { func apply(_ change: NoteChange, enqueueing operation: PendingOperation) async throws func pendingOperations() async throws -> [PendingOperation] func acknowledge(operationID: UUID, serverRevision: Int) async throws}The note change and outbox record must commit together. A crash cannot leave a visible edit that synchronization forgot, or an operation for an edit that never committed.
Make retries idempotent
The server records operationID. Repeating the same upload returns the original result instead of applying the mutation twice. A sync cursor tracks which remote changes the device has incorporated, but it is not a replacement for per-operation acknowledgement.
Choose and expose conflict policy
Last-write-wins is simple but can silently discard work. Field Notes can merge independent fields automatically and present a visible choice when both device and server changed the same body from one base revision. Preserve both versions until resolution succeeds.
A cache is disposable by contract. The note library and pending outbox are durable product state. Calling the network the only source of truth hides offline edits and makes transient availability control the interface.
Series navigation
- Previous: Part 67: Modularization with Swift Package Manager
- Next: Part 69: Concurrency architecture, isolation, cancellation, and lifecycle
- Series index: Zero to iOS Hero
References
- SwiftData provides an Apple persistence framework suitable for durable local models.
- URLSession supplies network transfer tasks and cancellation.