Data Access Object
A Data Access Object (DAO) exposes storage operations while hiding the database or file API. The feature asks for domain data rather than making storage queries itself.
iOS use case
A task list may start with an in-memory store for previews and later use SwiftData or Core Data. The screen should not change when persistence changes.
import Foundation
struct TaskRecord: Identifiable {
let id: UUID
var title: String
}
protocol TaskDataAccess {
func allTasks() async throws -> [TaskRecord]
func save(_ task: TaskRecord) async throws
}
actor MemoryTaskDAO: TaskDataAccess {
private var records: [UUID: TaskRecord] = [:]
func allTasks() -> [TaskRecord] {
records.values.sorted { $0.title < $1.title }
}
func save(_ task: TaskRecord) {
records[task.id] = task
}
}
let dao: any TaskDataAccess = MemoryTaskDAO()The view model depends on TaskDataAccess. A production implementation maps between TaskRecord and storage-specific models. The actor keeps this in-memory implementation safe when tasks save concurrently.
Production decisions
Keep transaction rules and query performance visible. A DAO should not silently fetch every record to filter one screen. SwiftData model contexts have actor and lifetime rules; isolate the concrete implementation accordingly. Test save, fetch, failure propagation, and migration separately from the UI.
The boundary is about storage semantics
TaskDataAccess exposes domain operations, not SQL, Core Data fetch requests, or SwiftData model contexts. MemoryTaskDAO is one implementation: its actor serializes reads and writes to the dictionary. The returned [TaskRecord] is sorted because dictionary order is not a display contract. A production DAO can implement the same interface while translating storage objects into domain records.
From example to an iOS feature
The task view model receives a TaskDataAccess in its initializer, calls allTasks() when the screen loads, and calls save(_:) after editing. A SwiftData implementation would own its ModelContext and perform work on the correct actor; a Core Data implementation would use its managed-object context queue. Do not pass live database objects into SwiftUI views if their lifetime and thread rules are unclear. Return snapshots or app-owned models.
Failure modes and verification
Test save, fetch ordering, duplicate IDs, and failures from the concrete storage layer. A single DAO should not grow into an entire business service. Put rules like “completed tasks cannot be edited” in the domain layer, then let the DAO persist the valid result. Pagination and filtered queries should be explicit methods when data size grows; fetching everything and filtering in a view model can become slow.