top of page

CFA Reference: Upgrading an Existing App

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

An upgrade replaces GCD, operation queues and callbacks with CFA ownership and Swift Concurrency while preserving product behaviour. On this site the upgrade is always done as one whole-app change: there is no hybrid release, and the work is not called complete until the change gate passes for the whole app.

Definitions

  • Baseline: A recorded analysis of the project before changes, made with node scripts/project-analysis/project-analysis.mjs <project-root> --out <new-analysis.json>.
  • Behaviour preservation: The upgraded app does what the old app did, including its failure and empty states.
  • Concurrency inventory: A list of every Task, actor and isolation boundary after the change; part of the completion record.
  • Whole-app upgrade: Every feature moves to CFA and Swift Concurrency in the same change, and the old concurrency code is removed.

Rules

  1. Commit existing work first, so the upgrade is a clean, reviewable change.
  2. Record the baseline analysis and a before navigator map.
  3. Write a product behaviour contract from tests, source and manual journeys: behaviour, stored data, error delivery, ordering and cancellation policy.
  4. Inventory every queue, barrier, group and semaphore before changing code.
  5. Move every feature to a concrete @MainActor Feature with an actor-backed repository in the same change.
  6. Remove every DispatchQueue, operation queue, lock and callback-based API that the new code replaces. Bridge a callback only when no appropriate native async API exists, and account for exactly-once completion and cancellation.
  7. Pass the change gate for the whole app: after map, feature inventory, concurrency inventory, build and tests at the final commit, and a human architecture review.

Decision rules

  • When the app has GCD and callbacks, use the Legacy GCD migration skill.
  • When a modern app only needs CFA structure, use CFA Architecture Adoption.
  • When you are unsure how much the upgrade involves, run CFA Architecture Review and the Xcode Project Dashboard first.
  • When a part of the app cannot be upgraded yet, keep the whole upgrade open; do not ship a partly upgraded app.

Example

The legacy shape on the left is what the upgrade removes. It needs a serial queue, a hop to the main queue and @unchecked Sendable. The CFA shape replaces it with an actor repository and a Feature (see References 5 and 6).

Before: legacy callback store (compiled to show the starting point; this is a prohibited pattern in CFA)

import Foundation

// BEFORE: a legacy callback API (shown only as the starting point to remove)
final class LegacyHabitStore: @unchecked Sendable {
    private let queue = DispatchQueue(label: "habits")
    private var names: [String] = []

    func loadNames(completion: @escaping @Sendable ([String]) -> Void) {
        queue.async {
            let snapshot = self.names
            DispatchQueue.main.async { completion(snapshot) }
        }
    }
}

After: actor repository (compiled excerpt)

import Foundation
import Observation

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

Common mistakes

  • Upgrading one feature, releasing, and leaving the rest on GCD. The app then has two concurrency models.
  • Wrapping a callback in a continuation when a native async API exists, or keeping the old queues underneath the new async methods.
  • Rewriting syntax without moving ownership to Features.
  • Changing the product's behaviour or design during the upgrade.
  • Calling the upgrade complete before the tests pass at the final commit.

Links

bottom of page