Skip to content

UIKit observation, concurrency, networking, and persistence

UIKit does not require tangled callbacks. An observable presentation model can expose one state, while a controller owns observation lifetime and converts state into imperative rendering.

Keep UI state on the main actor

@MainActor
@Observable
final class NoteListModel {
private let library: NoteLibrary
private(set) var state: LoadState<[FieldNote]> = .idle
func load() async {
state = .loading(previous: state.value)
do { state = .loaded(try await library.notes()) }
catch is CancellationError { return }
catch { state = .failed("Notes unavailable", previous: state.value) }
}
}

The library hides persistence and network adapters. The controller never builds SQL, HTTP requests, or file paths.

Own task and observation lifetime

private var loadTask: Task<Void, Never>?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
loadTask = Task { await model.load() }
}
deinit { loadTask?.cancel() }

Production code also avoids duplicate loads and cancels when represented input changes. Long-lived callbacks capture controllers weakly unless ownership is intentional.

Render snapshots on the main actor

Decode and transform large payloads away from the UI actor when safe. Return sendable values, then update model state and apply collection snapshots on the main actor. Cancellation blocks stale results from replacing newer data.

Preserve useful state

First load, refresh with previous content, offline content, error, permission denial, and empty results render differently. Persistence errors preserve the draft or previous list when possible.

Validation boundary

Observation wiring, actor behavior, task cancellation, URLSession, persistence, collection snapshots, and lifecycle integration remain Not verified.

Series navigation

References