Why use AppModel in our code?
import Foundation
/// The composition root for the application.
///
/// AppModel constructs and exposes Features. Application behavior lives
/// in those features.
@MainActor
final class AppModel {
/// The single instance of our model. Use it to access Features.
static let shared = AppModel.live()
let weightEntryFeature: WeightEntryFeature
let progressFeature: ProgressFeature
let settingsFeature: SettingsFeature
let habitsFeature: HabitsFeature
let purchaseFeature: PurchaseFeature
let backupFeature: BackupFeature
let weightImportFeature: WeightImportFeature
let dailyTipsFeature: DailyTipsFeature
private var hasLaunched = false
init(
weightEntryFeature: WeightEntryFeature,
progressFeature: ProgressFeature,
settingsFeature: SettingsFeature,
habitsFeature: HabitsFeature,
purchaseFeature: PurchaseFeature,
dailyTipsFeature: DailyTipsFeature,
backupFeature: BackupFeature,
weightImportFeature: WeightImportFeature
) {
self.weightEntryFeature = weightEntryFeature
self.progressFeature = progressFeature
self.settingsFeature = settingsFeature
self.habitsFeature = habitsFeature
self.purchaseFeature = purchaseFeature
self.dailyTipsFeature = dailyTipsFeature
self.backupFeature = backupFeature
self.weightImportFeature = weightImportFeature
}
/// Produces the live, non-test AppModel and assembles all production
/// dependencies in one visible place.
static func live() -> AppModel {
// A function, rather than a stored Date, keeps production time moving
// while allowing tests to provide a fixed clock.
let currentDate: @MainActor () -> Date = { .now }
let directory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appending(path: "Trend", directoryHint: .isDirectory)
let storage = LocalDataStore(directory: directory,
cloudContainerIdentifier: "iCloud.com.3DaysOfSwiftConcurrency.Trend.SwiftData")
let repository = LocalWeightRepository(storage: storage)
let weightLog = WeightLogManager(repository: repository)
let progress = ProgressTracker(currentDate: currentDate)
let settings = UserSettingsStore(cloudSync: ICloudDriveAvailability())
let dailyTrend = DailyTrendManager()
let dailyTipsFeature = DailyTipsFeature(currentDate: currentDate)
let dailyStreak = DailyStreakManager(trend: dailyTrend, currentDate: currentDate)
let backupFiles = BackupFileManager()
let habitDataStore = LocalHabitRepository(storage: storage)
let habitsFeature = HabitsFeature(
storage: habitDataStore,
currentDate: currentDate
)
let purchaseFeature = PurchaseFeature(client: StoreKitPurchaseClient())
let weightEntryFeature = WeightEntryFeature(
weightLog: weightLog,
progress: progress,
settings: settings,
dailyTrend: dailyTrend,
dailyStreak: dailyStreak,
currentDate: currentDate
)
let progressFeature = ProgressFeature(
progress: progress,
weightLog: weightLog,
settings: settings
)
let settingsFeature = SettingsFeature(
settings: settings,
weightLog: weightLog,
progress: progress,
dailyStreak: dailyStreak,
backupFiles: backupFiles
)
let backupFeature = BackupFeature(storage: storage,
files: RecoveryBackupFiles(folder: directory.appending(path: "Backups")), settings: settings) {
await weightEntryFeature.refresh()
await habitsFeature.reload()
}
return AppModel(
weightEntryFeature: weightEntryFeature,
progressFeature: progressFeature,
settingsFeature: settingsFeature,
habitsFeature: habitsFeature,
purchaseFeature: purchaseFeature,
dailyTipsFeature: dailyTipsFeature,
backupFeature: backupFeature,
weightImportFeature: WeightImportFeature(weightLog: weightLog) { await weightEntryFeature.refresh() }
)
}
/// Registers observation immediately and starts independent loads once.
func applicationDidFinishLaunching() {
guard !hasLaunched else { return }
hasLaunched = true
purchaseFeature.observeTransactionUpdates()
dailyTipsFeature.refresh()
backupFeature.observeLocalChanges()
backupFeature.observeCloudChanges()
Task { await backupFeature.triggerDataBackupIfNeeded() }
Task { await purchaseFeature.refreshStoreState() }
Task { await settingsFeature.refreshCloudStatus() }
Task { await weightEntryFeature.refresh() }
Task { await habitsFeature.load() }
}
func applicationDidBecomeActive() {
Task {
await weightEntryFeature.refresh()
await habitsFeature.reload()
await backupFeature.triggerDataBackupIfNeeded()
}
}
func applicationSignificantTimeChange() {
Task { await habitsFeature.updateCurrentDay() }
}
}This is the complete AppModel class from Trend, our weight-tracking app. Its feature properties tell us what the program can do. Its construction code connects those features, and its lifecycle methods notify them when the application launches or becomes active.
CFA uses AppModel as the entrance to the application’s model. Business logic belongs in the features behind that entrance. A screen’s ViewModel obtains its feature here, then works through that feature’s API. AppModel makes the boundary visible without becoming the place where all the work happens.
1. The approach
let weightEntryFeature: WeightEntryFeature
let progressFeature: ProgressFeature
let settingsFeature: SettingsFeature
let habitsFeature: HabitsFeature
let purchaseFeature: PurchaseFeature
let backupFeature: BackupFeature
let weightImportFeature: WeightImportFeature
let dailyTipsFeature: DailyTipsFeatureBegin with these properties from the opening class. Weight recording, progress, settings, habits, purchases, backups, importing and daily tips are visible before we inspect their implementation. AppModel is a catalogue of the application’s capabilities.
Each property leads to a Feature that owns authoritative application state and operations for a business capability. Those Features are MainActor-isolated, even when a particular capability has no screen yet. Networking sessions, database contexts and internal caches stay behind the Features; AppModel does not catalogue every object with mutable implementation details.
AppModel does not need a forwarding method for every purchase, import or chart command. Its feature catalogue and those feature APIs together provide access to the program.
This gives developers a route through unfamiliar code: find the capability in AppModel, then open its owner. It also gives an AI assistant a concrete starting point for a screen: inspect the feature’s available state and commands before designing its ViewModel and View. That is useful architectural guidance, not a guarantee of correct generated UI.
2. The problem it solves
import Foundation
import Observation
/// Presents the progress feature as a complete capability without exposing the
/// managers that calculate it or the stores that supply its inputs.
@MainActor
@Observable
final class ProgressFeature {
private let progress: ProgressTracker
private let weightLog: WeightLogManager
private let settings: UserSettingsStore
init(
progress: ProgressTracker,
weightLog: WeightLogManager,
settings: UserSettingsStore
) {
self.progress = progress
self.weightLog = weightLog
self.settings = settings
}
var progressSnapshot: WeightHistoryData { progress.snapshot }
var goalWeightKilograms: Double? { weightLog.goalKilograms }
var selectedWeightUnit: WeightUnit { settings.unit }
var isPreparingProgress: Bool { progress.isLoading }
var selectedProgressRange: ProgressRange { progress.range }
func selectProgressRange(_ range: ProgressRange) {
progress.select(
range,
entries: weightLog.entries,
goalKilograms: weightLog.goalKilograms
)
}
}This complete class is Trend’s progress feature. Suppose we are adding a control that changes the period shown on the chart. Where should the code that selects measurements and prepares chart results belong? Not in the picker, not in AppModel, but in this feature and its supporting model objects.
selectProgressRange supplies the tracker with the current measurements and goal. The screen requests a range and reads the resulting progressSnapshot. It does not have to discover those dependencies itself.
That answers the larger question: “Where does this code belong?” Code that changes the main workings of the program belongs below the AppModel boundary. Screen layout, temporary drafts and presentation messages belong above it. Without a clear boundary, each new screen creates another opportunity to put shared decisions in UI code.
This rationale was confirmed by Matthew Thomas: AppModel makes the model boundary and the application’s capabilities explicit. A singleton does not enforce the boundary by itself. Developers still keep feature internals private and avoid moving business rules into AppModel.
3. Why it works
static let shared = AppModel.live()The first access constructs the production model through live(); later accesses return the same stored reference. Swift’s stored type-property rules provide that initialisation behaviour. CFA deliberately uses it as a narrow route from presentation to the application’s long-lived model.
This is not a special permanent region of RAM, and a singleton does not make mutable state thread-safe. The reference supplies a shared lifetime. Isolation and each feature’s operation policy remain separate responsibilities.
import Foundation
import Observation
@MainActor
@Observable
final class ProgressViewModel {
private let progressFeature: ProgressFeature
init(progressFeature: ProgressFeature = AppModel.shared.progressFeature) {
self.progressFeature = progressFeature
}
var snapshot: WeightHistoryData { progressFeature.progressSnapshot }
var goalKilograms: Double? { progressFeature.goalWeightKilograms }
var unit: WeightUnit { progressFeature.selectedWeightUnit }
var isLoading: Bool { progressFeature.isPreparingProgress }
var range: ProgressRange {
get { progressFeature.selectedProgressRange }
set { progressFeature.selectProgressRange(newValue) }
}
}Trend’s progress ViewModel defaults to the existing feature. After construction it uses its private progressFeature reference; it does not repeatedly search AppModel for unrelated capabilities. The View owns the ViewModel inline, so dependencies do not have to be threaded through the View hierarchy.
We have avoided dependency plumbing through Views, not dependency injection everywhere. AppModel’s factory still connects collaborators, and tests can pass isolated concrete features into ViewModels.
func applicationDidFinishLaunching() {
guard !hasLaunched else { return }
hasLaunched = true
purchaseFeature.observeTransactionUpdates()
dailyTipsFeature.refresh()
backupFeature.observeLocalChanges()
backupFeature.observeCloudChanges()
Task { await backupFeature.triggerDataBackupIfNeeded() }
Task { await purchaseFeature.refreshStoreState() }
Task { await settingsFeature.refreshCloudStatus() }
Task { await weightEntryFeature.refresh() }
Task { await habitsFeature.load() }
}This launch method registers observers and starts shared work through feature APIs. Its Boolean guard prevents a second launch sequence on this instance. The platform delegate calls it; individual screens do not have to trigger preparation for the whole application.
Starting early gives work an opportunity to overlap with UI construction. It does not guarantee that an asynchronous load finishes before the first frame. A screen’s .task remains appropriate for screen-owned work; it is not inherently too slow. The distinction is ownership. Features still need to coordinate overlapping refreshes and expose their own failures.
4. Advantages of using it
@Test func settingsCommandsRemainBehindAppModelBoundary() async {
let repository = InMemoryWeightRepository(cloudStatus: .available)
let appModel = TestAppModelFactory.make(repository: repository)
appModel.settingsFeature.setWeightUnit(.pounds)
await appModel.settingsFeature.refreshCloudStatus()
#expect(appModel.settingsFeature.selectedWeightUnit == .pounds)
#expect(appModel.settingsFeature.cloudSyncStatus == .available)
}This test changes settings through an isolated AppModel and reads the resulting feature state. It does not construct a Settings screen or mutate the production singleton. Keeping behaviour below the UI boundary allows another entry point to exercise the same program. A command-line interface could do so too, with suitable replacements for platform-specific services.
For screen development, the catalogue is equally practical. A developer or AI assistant can find the progress capability, inspect its snapshot and commands, then build presentation around that contract. The designer still controls navigation, visual identity and accessibility.
Editorial trade-off: global production defaults are convenient but easy to misuse. Keep shared access limited to the declared UI-to-model route and lifecycle entry points. Tests build separate graphs; features receive their collaborators rather than looking them up globally.
dailyTipsFeature belongs in that catalogue too. It owns the tip-selection behaviour and current optional suggestion. The Today ViewModel uses it alongside weightEntryFeature to present a completed check-in. Weight recording does not need to know how tips are selected. A screen can present several independent capabilities, while each capability keeps its own responsibility.
AppModel exposes its Features as read-only references. A ViewModel can call dailyTipsFeature.nextTip(), but cannot replace the shared Feature. The Feature keeps its storage and selection details private. This makes the capability accessible without exposing its implementation.
Try this with the CFA AI Toolkit for iOS:
Using CFA for iOS, make this application’s composition and test isolation explicit.
Scope: code from the linked app revisions; smaller excerpts belong to their containing types and targets. The articles distinguish older implementations from current CFA conventions. Source verification is not a full app build or device test. Suggested checks are guidance, not claims they ran against your application.