top of page

Why use an ownership-based project structure in our code?

Code from Trend • Full Xcode project ↗
import Foundation

/// Presents the progress feature as a complete capability without exposing the
/// managers that calculate it or the stores that supply its inputs.
@MainActor
final class ProgressManager {
    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 class lives in Trend/2 - AppModel/Features/Progress/ProgressManager.swift. Its location tells us where to begin looking for Trend’s progress capability; its code tells us what that capability actually owns. Both forms of information are useful, but only the code establishes the boundary.

CFA’s folder structure is a map for people and AI working in Xcode. It should make the route from screen to feature to storage easy to follow, without inventing extra types merely to fill folders.

1. The approach

Code from Trend • Full Xcode project ↗
import Foundation
import Observation

@MainActor
@Observable
final class ProgressViewModel {
    private let progress: ProgressManager

    init(progress: ProgressManager = AppModel.shared.progressFeature) {
        self.progress = progress
    }

    var snapshot: WeightHistoryData { progress.progressSnapshot }
    var goalKilograms: Double? { progress.goalWeightKilograms }
    var unit: WeightUnit { progress.selectedWeightUnit }
    var isLoading: Bool { progress.isPreparingProgress }
    var range: ProgressRange {
        get { progress.selectedProgressRange }
        set { progress.selectProgressRange(newValue) }
    }
}

This complete ViewModel lives in Trend/1 - View/Views/Progress/ProgressViewModel.swift. Its job is presentation: expose progress information and translate a range selection into a feature command. It belongs beside the progress screen, not inside the calculation or storage implementation.

Trend’s top-level separation is practical: 1 - View contains presentation, 2 - AppModel contains application behaviour and its supporting data access, and 3 - App Resources contains assets and platform resources. The numbers are a navigation aid, not a Swift language requirement.

CFA’s documented rationale: make ownership discoverable. A developer should be able to predict where an unfamiliar responsibility belongs and find it without searching a generic collection of managers, helpers and miscellaneous files.

2. The problem it solves

Code from Trend • Full Xcode project ↗
let progressFeature = ProgressManager(
    progress: progress,
    weightLog: weightLog,
    settings: settings
)

AppModel connects the progress feature to the existing tracker, log and settings. These construction statements are the important evidence of cooperation. Moving all three types into a folder named Progress would not, on its own, establish whether they are shared or independently constructed.

A folder named Services can hide unrelated ownership. So can one enormous AppModel file. The structure should help us ask precise questions: who owns the measurements, who calculates progress, and who formats the screen? If two types own the same state without a real distinction, adding another folder does not solve it.

Likewise, a top-level Package.swift is not automatically an AI leftover. It may define a genuine library, test harness or tool. Check its consumers and the project’s documented build before proposing removal. File names alone cannot establish that something is unnecessary.

3. Why it works

Code from Trend • Full Xcode project ↗
import Foundation

protocol WeightRepository: Sendable {
    func load() async throws -> WeightStore
    func save(_ store: WeightStore) async throws
}

struct WeightStore: Codable, Sendable {
    var entries: [WeightEntry]
    var goalKilograms: Double?
}

This contract lives under 2 - AppModel/User Data Storage/Protocols. The production implementation and test double can differ while callers use the same load/save capability. That is why it belongs with persistence, not with a View that happens to display the data.

The concrete feature remains the screen’s entry point. The repository contract is an implementation dependency below it. This direction matters more than whether Xcode displays the folders with a particular colour. The old whole-store save API shown here is not a recommendation to replace collections for individual edits.

Moving a file also requires checking its target membership, resource references and imports. Swift code is not compiled merely because it appears somewhere in the repository. Follow the project’s actual build configuration and Apple’s local-package guidance when a package genuinely owns part of the build.

4. Advantages of using it

Code from Trend • Full Xcode project ↗
@Test func exposesSnapshotGoalUnitAndLoadingState() async {
    let now = Date()
    let entries = [
        WeightEntry(date: now.addingTimeInterval(-86_400), kilograms: 82),
        WeightEntry(date: now, kilograms: 80)
    ]
    let repository = InMemoryWeightRepository(store: .init(entries: entries, goalKilograms: 75))
    let appModel = TestAppModelFactory.make(repository: repository, unit: .pounds)
    await appModel.weightEntries.refresh()
    let viewModel = ProgressViewModel(progress: appModel.progressFeature)

    #expect(viewModel.snapshot.points.map(\.kilograms) == [82, 80])
    #expect(viewModel.snapshot.changeKilograms == -2)
    #expect(viewModel.goalKilograms == 75)
    #expect(viewModel.unit == .pounds)
    #expect(!viewModel.isLoading)
}

The matching test lives under TrendTests/View model tests/ProgressViewModelTests.swift. Its location names the presentation boundary under test, and its assertions establish useful behaviour. A new contributor can move from the ViewModel file to its test without learning an unrelated classification system.

Editorial trade-off: a rigid folder template can become busywork if applied without understanding ownership. Keep related code together, use the existing feature when it already owns the responsibility, and avoid a new wrapper type for every file. Older “Manager” names in these sources can be improved in an authorized code change; this article does not rename the app.

When cleaning a project, first identify each file’s purpose, consumers and target. Remove only verified leftovers, keep an inspectable diff, and build the affected targets afterward. Check the built app’s resources and configuration too. A neat navigator is useful; a working, understandable project is the goal.

Try this with the CFA AI Toolkit for iOS:

Using CFA for iOS, review project organization and identify unused files before proposing any removal.

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.

bottom of page