CFA Reference: Isolation Defaults
Formal CFA AI Toolkit for iOS Documentation · Reference 7 of 26
CFA makes isolation visible in the source. Features and ViewModels are marked @MainActor explicitly, shared mutable storage lives in actors, and only Sendable values cross between them. The toolkit's projects use Swift 6 language mode (SWIFT_VERSION 6.0).
Definitions
- @MainActor: Global-actor isolation to the main actor. CFA writes it on each Feature, ViewModel and AppModel.
- Actor: A reference type that protects its own mutable state. CFA uses actors for repositories and managers such as SwiftDataManager.
- Sendable: A type that is safe to pass across isolation boundaries. Domain values, such as Habit, are Sendable structs.
- Region-based isolation: The Swift 6 checker that reports “sending ... risks causing data races” when a non-Sendable value is passed to another isolation domain and then used again.
- @ObservationIgnored: An Observation attribute that excludes a stored property, such as a task handle, from change tracking.
Rules
- Write @MainActor on every Feature, ViewModel and AppModel. Do not rely on a project-wide default to supply it.
- Put shared mutable storage in an actor. Do not protect it with locks or serial dispatch queues.
- Make domain values Sendable structs.
- Do not use @unchecked Sendable as a compiler-warning escape hatch.
- Do not perform expensive or blocking work on the main actor; move it to the repository actor.
- Capture only what a Task needs, for example [repository], and keep the work's result on the Feature's actor.
Decision rules
- When a type owns UI-observed state, mark it @MainActor and @Observable.
- When a type owns storage or I/O state shared between callers, make it an actor.
- When a value crosses from a repository to a Feature, make it a Sendable struct.
- When the compiler reports a data-race error, fix the ownership; never silence it with an unsafe opt-out.
- When existing code relies on implicit isolation, make it explicit during the upgrade so a reader can see it.
Example
The domain value is Sendable, the repository is an actor, and the Feature is explicitly @MainActor. The Feature captures the repository, a Sendable existential, in its task.
Compiled example (excerpt)
import Foundation
import Observation
// MARK: Domain value owned by the Habits feature
struct Habit: Sendable, Identifiable, Equatable {
let id: UUID
var name: String
}
enum HabitError: Error, Equatable {
case invalidName
}
// MARK: Repository contract in domain language
protocol HabitsRepository: Sendable {
func fetchHabits() async throws -> [Habit]
func insertHabit(_ habit: Habit) async throws
}
// MARK: Actor-isolated implementation
actor LocalHabitsRepository: HabitsRepository {
private var storedHabits: [Habit] = []
func fetchHabits() async throws -> [Habit] {
storedHabits
}
func insertHabit(_ habit: Habit) async throws {
storedHabits.append(habit)
}
}Compiled with Swift 6 language mode and warnings treated as errors: type-checked on Swift 6.2 and built on Swift 6.4 (Linux toolchains, Foundation and Observation only). The matching @MainActor Feature is in Reference 5.
Common mistakes
- Leaving isolation implicit, so a reader cannot tell where a method runs.
- Marking a repository @MainActor, which puts I/O on the main actor.
- Adding nonisolated(unsafe) or @unchecked Sendable to make an error disappear.
- Making a class with mutable stored properties conform to a Sendable protocol; Swift reports “stored property ... of 'Sendable'-conforming class ... is mutable”.
