Concurrency architecture, isolation, cancellation, and lifecycle
Concurrency architecture answers two questions before tasks are launched: who owns each mutable value, and what lifetime ends the work?
Draw an isolation map
MainActor: visible editor, routes, presentation phaseSyncEngine actor: outbox pass, cursor, retry stateNoteStore actor: transaction and durable mutation orderingSendable values: notes, changes, DTOs, acknowledgementsUnisolated work: pure decoding, validation, and formattingactor SyncEngine { private var activeRun: Task<Void, Never>?
func start() { guard activeRun == nil else { return } activeRun = Task { [weak self] in await self?.drainOutbox() } }
func stop() { activeRun?.cancel() activeRun = nil }}The real drain loop checks cancellation before expensive work and between operations. Cancellation is a control signal, so cleanup and transaction rollback still run.
Tie work to a lifetime
A search task belongs to the current query. An editor save belongs to the save command and may outlive one render, but not necessarily the scene. Background synchronization belongs to the approved background task window. Store each task where its owner can cancel it.
Actors are ownership boundaries
An actor protects its isolated state. It is not merely a queue for arbitrary work, and making every service an actor can create unnecessary hops without clarifying ownership. Use detached tasks only when work truly has no inherited priority, actor context, task-local values, or structured parent lifetime.
Series navigation
- Previous: Part 68: Data architecture, source of truth, caching, offline sync, and conflict
- Next: Part 70: Architecture tests, refactoring seams, decisions, and tradeoffs
- Series index: Zero to iOS Hero
References
- Swift concurrency covers tasks, cancellation, actors, and sendable values.
- MainActor defines the global actor used for UI state.