CFA Reference: Layers
Formal CFA AI Toolkit for iOS Documentation · Reference 3 of 26
CFA uses a fixed path through every feature. Each layer has one job and knows only about the layer to its right.
Definitions
- View: A SwiftUI View. It says what the screen looks like and forwards user intent to its ViewModel.
- ViewModel: An @MainActor @Observable class dedicated to one screen. It holds screen state, such as a draft or a presented sheet, and prepares Feature state for display.
- Feature API: The Feature's own non-private properties and methods. There is no matching FeatureAPI protocol.
- Feature: The concrete owner of business rules, domain data, operation state and errors for one area of the product.
- Repository: A protocol in domain language with a concrete implementation that reads and writes data: a JSON file cache, SwiftData store or web service.
- External System: Anything outside the app process boundary: the file system, a database, the network, the operating system.
Rules
- Calls travel from View to ViewModel to Feature to Repository. Observed state travels back by Observation.
- A View never calls a Feature or repository directly.
- A ViewModel never stores a copy of a Feature's domain data; it reads it through computed properties.
- A Feature never imports SwiftUI types for layout and never knows which screen is showing it.
- A repository speaks in domain values, for example fetchHabits() returning [Habit], not raw rows or response objects.
- Lower layers do not reach back up. A repository never calls a Feature.
Decision rules
- When the logic answers “what does the screen show?”, put it in the ViewModel.
- When the logic answers “what is true for the product?”, put it in the Feature.
- When the logic answers “how is it stored or fetched?”, put it in the repository.
- When two screens need the same data, both ViewModels read the same Feature instance from AppModel; neither keeps its own copy.
Example
The ViewModel below reads the Feature's state through computed properties and forwards work by calling Feature methods. It owns only the draft text and its own save error.
Compiled example (excerpt; the Feature and AppModel are in References 4 and 5)
import Foundation
import Observation
// MARK: Screen ViewModel
@MainActor
@Observable
final class HabitsViewModel {
private let habitsFeature: HabitsFeature
@ObservationIgnored private var refreshTask: Task<Void, Never>?
var draftName = ""
private(set) var saveError: String?
init(habitsFeature: HabitsFeature = AppModel.shared.habitsFeature) {
self.habitsFeature = habitsFeature
}
var habitNames: [String] { habitsFeature.habits.map(\.name) }
var isLoading: Bool { habitsFeature.isLoading }
var loadError: String? { habitsFeature.loadError }
/// Repeated presses reuse the one retained task.
@discardableResult
func refresh() -> Task<Void, Never> {
if let refreshTask { return refreshTask }
let habitsFeature = habitsFeature
let newRefreshTask = Task {
await habitsFeature.refresh()
self.refreshTask = nil
}
refreshTask = newRefreshTask
return newRefreshTask
}
func save() async {
do {
try await habitsFeature.addHabit(named: draftName)
draftName = ""
saveError = nil
} catch {
saveError = "The habit could not be saved."
}
}
}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).
Common mistakes
- Putting business rules, such as name validation, in the View or ViewModel. HabitsFeature.addHabit(named:) trims and validates the name.
- Copying Feature data into ViewModel stored properties, which creates a second owner that drifts.
- Passing repository response types up to the View.
- Adding a layer, such as a coordinator or interactor, that CFA does not define.
