top of page

CFA Reference: AppModel Composition Root

Formal CFA AI Toolkit for iOS Documentation · Reference 4 of 26

AppModel is the composition-root type. It creates the real Features and repositories in one place, exposes them through one shared instance, and starts launch work once.

Definitions

  • Composition root: The single place where concrete dependencies are created and connected.
  • AppModel.shared: static let shared = live(), the one production instance.
  • live(): A single static factory that builds every repository and Feature and passes them to the explicit initialiser.
  • Launch guard: One private Boolean that makes applicationDidFinishLaunching() safe to call more than once.
  • App delegate: The UIApplicationDelegate attached with @UIApplicationDelegateAdaptor (CFAAppDelegate in the CFA App template). Its didFinishLaunchingWithOptions method calls AppModel.shared.applicationDidFinishLaunching().

Rules

  1. AppModel stores only static let shared, the concrete Features, and one private launch Boolean (change gate rule 14).
  2. The initialiser is explicit and takes every Feature as a parameter, so tests can build an isolated graph.
  3. Exactly one static func live() builds the production graph.
  4. applicationDidFinishLaunching() checks its guard, sets it, and synchronously calls each Feature's startup method (rule 9). It does not await anything; returning confirms startup was initiated, not that loading has finished.
  5. Repositories are created in live() and handed to Features; AppModel does not store them separately.
  6. Tests never use AppModel.shared; they construct their own AppModel.

Decision rules

  • When a new Feature is added, add one stored property, one initialiser parameter and one line in live().
  • When a Feature needs launch work, give it a synchronous startLoading() and call it from applicationDidFinishLaunching().
  • When two Features need the same data, pass the same Feature instance into the second Feature in live(), as Habit Tracker does when TodayFeature receives HabitsFeature.
  • When a test needs the app graph, call the explicit initialiser with test repositories.

Example

This compiled AppModel stores two Features and one launch Boolean, builds everything in live(), and starts launch work synchronously.

Compiled example (excerpt)

import Foundation
import Observation

// MARK: Composition root
@MainActor
@Observable
final class AppModel {
    static let shared = live()

    let habitsFeature: HabitsFeature
    let settingsFeature: SettingsFeature
    private var didFinishLaunching: Bool

    init(habitsFeature: HabitsFeature, settingsFeature: SettingsFeature) {
        self.habitsFeature = habitsFeature
        self.settingsFeature = settingsFeature
        didFinishLaunching = false
    }

    static func live() -> AppModel {
        let habitsRepository = LocalHabitsRepository()
        return AppModel(
            habitsFeature: HabitsFeature(repository: habitsRepository),
            settingsFeature: SettingsFeature(defaults: .standard)
        )
    }

    func applicationDidFinishLaunching() {
        guard !didFinishLaunching else { return }
        didFinishLaunching = true
        habitsFeature.startLoading()
    }
}

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).

In the CFA App template, the app entry point attaches the delegate like this:

Repository excerpt, shown for reading. It uses SwiftUI or SwiftData, so it was not compiled for this page. Taken from the CFA App template's App file; ___PACKAGENAME:identifier___ is replaced with your product name when Xcode creates the project.

import SwiftUI
import UIKit

@MainActor
final class CFAAppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _: UIApplication,
        didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        AppModel.shared.applicationDidFinishLaunching()

        return true
    }
}

@main
struct ___PACKAGENAME:identifier___App: App {
    @UIApplicationDelegateAdaptor(CFAAppDelegate.self) private var appDelegate
    @State private var themeManager = ThemeManager.shared

    var body: some Scene {
        WindowGroup {
            RootTabView()
                .environment(themeManager)
                .preferredColorScheme(themeManager.selectedTheme.colorScheme)
        }
    }
}

Common mistakes

  • Storing repositories, caches or ViewModels on AppModel.
  • Having several factory methods (live, preview, mock) that drift apart. Tests use the explicit initialiser instead.
  • Awaiting network work inside applicationDidFinishLaunching(), which delays launch.
  • Starting launch work from a View's .task as the only app-wide launch mechanism.
  • Reaching for AppModel.shared inside a Feature. Features receive their dependencies through the initialiser.

Links

bottom of page