top of page

Why use cohesive features 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 is the complete progress feature in Trend. It lets a person inspect how their recorded weight has changed over time. The feature exposes chart data, a goal, a weight unit, a preparation flag and a command for selecting the period. Those members belong together because they serve one capability.

A CFA Feature owns authoritative application state for a business capability and the operations that change it. Authoritative means that other callers rely on this owner to decide what the application currently knows. A folder or tab name alone does not establish that responsibility.

1. The approach

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

AppModel connects this feature to the existing progress tracker, weight log and settings store. The same weight log can support the recording interface without being recreated for the progress tab. Separate capabilities do not require separate copies of the measurements.

The feature’s dependencies stay private. Its caller asks for progress; it does not directly reach into the repository or select a calculation helper. This older application uses ProgressManager. In current CFA, the corresponding concrete owner would be named ProgressFeature, with a matching progressFeature property.

CFA’s documented rationale: give an application capability an identifiable owner and keep shared business state authoritative. A screen can present several independent business capabilities. One Feature can support several screens or device experiences; a new tab does not automatically require a new Feature. This rationale was confirmed by Matthew Thomas.

For example, a Settings ViewModel can use a SettingsFeature for preferences and a UserProfileFeature for its profile header. The SettingsFeature does not need to know about the profile merely because its screen displays an avatar. The ViewModel brings the capabilities together for that screen, while the profile keeps one authoritative owner.

In a new CFA app, begin with independent Features. Trend’s older progress capability combines existing model owners; treat that as an architectural extension for a business calculation, not the default dependency pattern for every screen. A ViewModel combining a profile header with settings does not need another Feature to combine them.

For a downloaded profile, UserProfileFeature owns the accepted current profile and its refresh outcome. Its API provider fetches values. For a feed, PostFeedFeature owns the accepted posts, pagination cursor and loading or failure state. A networking service may track requests or cache responses internally; that machinery does not make it a second authority for the feed.

All CFA Features use MainActor isolation and are available through AppModel, including capabilities that have no screen yet. Services use isolation appropriate to their work. This is CFA’s architectural convention, not a Swift requirement that every mutable object use MainActor.

2. The problem it solves

Code from Trend • Full Xcode project ↗
enum ProgressRange: String, CaseIterable, Identifiable, Sendable {
    case month = "1M"
    case threeMonths = "3M"
    case sixMonths = "6M"
    case year = "1Y"
    case all = "All"
    var id: Self { self }
    var days: Int? {
        switch self {
        case .month: 30
        case .threeMonths: 90
        case .sixMonths: 180
        case .year: 365
        case .all: nil
        }
    }
}

Trend offers named periods with a defined meaning: one month is 30 days here, one year is 365, and All has no day limit. These are product decisions, not merely the labels of a segmented control.

If the chart View, a statistics sheet and an export screen each translated those labels independently, their results could differ. Moving the mapping into a domain type gives all callers the same interpretation. The UI can choose how to present the choices without redefining them.

The same reasoning applies to the measurements. Creating a new weight log for every tab would create multiple potential authorities. The composition shown above deliberately reuses one owner. The progress feature contributes a distinct capability, not a competing copy of recorded history.

3. Why it works

Code from Trend • Full Xcode project ↗
func selectProgressRange(_ range: ProgressRange) {
    progress.select(
        range,
        entries: weightLog.entries,
        goalKilograms: weightLog.goalKilograms
    )
}

The command combines the selected range with the weight log’s entries and goal. The tracker is responsible for preparation; this feature coordinates the inputs. A caller cannot accidentally omit the goal by choosing a different calculation path through the UI.

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

The ViewModel shows the boundary from the other side. Its range setter calls the feature command. Its snapshot and preparation properties read the feature’s outputs. The feature owns the capability, the tracker performs the calculation, and the ViewModel exposes what the screen needs.

Notice that the feature does not declare a second stored copy of every tracker property. Its getters preserve a single source of truth. Swift computed properties are useful for this kind of narrow access, provided reading them remains cheap and free of side effects.

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

This test supplies known measurements, prepares progress, then reads the ViewModel. The expected points, change and goal describe the capability’s useful output. No tab bar is needed to establish that the pieces cooperate.

Editorial trade-off: a feature that only wraps another object without a distinct responsibility can become ceremony. Here its purpose is concrete: coordinate the progress capability while hiding the weight log, settings and calculation collaborators from presentation. Do not create another feature solely because a screen contains a second visual section.

When a ViewModel grows, ask: is it arranging existing capabilities for this screen, or implementing a new business capability itself? Only the latter points towards extracting business behaviour into the model. Device-specific layout and presentation coordination remain in the presentation layer. An architectural extension should clarify a newly identified responsibility; it should not exist merely to reduce the number of Features used by a ViewModel.

When adding your own capability, write down what it owns and which state it must share. Add its entry to AppModel, give it a narrow API, and make the screen use that API. This makes the application easier to navigate without pretending that a folder arrangement can replace ownership decisions.

Try this with the CFA AI Toolkit for iOS:

Using CFA for iOS, identify the owner of each capability and shared value in this app.

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