top of page

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

  1. Write @MainActor on every Feature, ViewModel and AppModel. Do not rely on a project-wide default to supply it.
  2. Put shared mutable storage in an actor. Do not protect it with locks or serial dispatch queues.
  3. Make domain values Sendable structs.
  4. Do not use @unchecked Sendable as a compiler-warning escape hatch.
  5. Do not perform expensive or blocking work on the main actor; move it to the repository actor.
  6. 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”.

Links

bottom of page