top of page

01 · INTRODUCTION

What Is Modern iOS Architecture?

Modern iOS architecture is SwiftUI MVVM with cooperative feature managers and worker actors.

Those terms are the answer:

SwiftUI declaratively renders the current state.

MVVM gives each substantial screen a tightly coupled ViewModel that prepares presentation state and receives user intent.

Cooperative feature managers provide readable product APIs, own business behaviour and observable feature state, and cooperate with cancellation and other features.

Worker actors protect independent mutable subsystems or perform concurrent preparation away from UI isolation when the product actually needs that boundary.

EXECUTION DIAGRAM

SwiftUI View
    ↓ user intent
Tightly coupled ViewModel
    ↓ product operation
Cooperative Feature Manager
    ↓ persistence or independent work
Repository / Worker Actor
    ↑ result and observable state

This is not a universal law from Apple, and it is not a demand that every feature contain every layer. It is the architecture we are proving in a real application. It keeps the interface declarative, gives business behaviour a testable home, makes concurrency part of the design and remains simple enough for a team to read.

The complete answer

Use SwiftUI to describe the interface, a ViewModel to serve one screen, a feature manager to own the product behaviour, and a worker actor only when independent isolation or concurrent work is required.

02 · TUTORIAL

Trend: The Open-Source App Behind This Architecture

Every architectural claim in this article is being tested in Trend, an open-source Swift 6 weight-tracking application. Trend contains real product features: daily check-ins, history, progress projections, habits, settings, StoreKit purchases, local storage, private CloudKit synchronization and automated tests.

The code is not a collection of disconnected teaching samples. It is one application in which a design decision made for Today can affect History, Progress, persistence, synchronization and testability.

Open-source reference app

3DaysOfSwift / TrendSwift 6 · iOS 17+

Explore the Complete Trend Architecture

Open the Xcode project, inspect the real feature boundaries and tests, run Trend on an iPhone, and challenge every architectural decision described in this article.

1. SwiftUI View

2. ViewModel

3. Feature Manager

4. Worker Actor / Repository

Complete source code · Feature tests · CloudKit and local persistence

View Trend on GitHub →

Trend states its architecture in one readable line:

EXECUTION DIAGRAM

View → ViewModel → Feature API → Feature Manager → Repository

A worker actor can sit behind a feature manager when the feature owns a concurrently accessed subsystem or work that should not occupy the main actor. It is an implementation boundary, not another ceremonial layer that every action must traverse.

03 · TUTORIAL

The Folder Hierarchy Matches the Architecture

The Xcode navigator should reveal how the application works. Trend therefore places presentation together and groups model code by product feature:

EXECUTION DIAGRAM

Trend
├── 1 - View
│   ├── TrendApp.swift
│   ├── Theme
│   ├── SwiftUI Extensions
│   └── Views
│       ├── Today
│       │   ├── TodayView.swift
│       │   └── TodayViewModel.swift
│       ├── EntryEditor
│       ├── Progress
│       ├── History
│       ├── Settings
│       └── Habits
├── 2 - AppModel
│   ├── AppModel.swift
│   ├── Features
│   │   ├── Daily Streak
│   │   ├── Daily Tips
│   │   ├── Daily Trend
│   │   ├── Habits
│   │   ├── Progress
│   │   ├── Purchases
│   │   ├── Settings
│   │   └── WeightLog
│   └── User Data Storage
│       ├── Protocols
│       ├── Local
│       └── CloudKit
├── 3 - App Resources
└── 4 - Swift Extensions

TrendTests
├── View model tests
├── AppModel tests
└── Test Support

The hierarchy and the architecture tell the same story. 1 - View contains the declarative interface and screen-specific ViewModels. 2 - AppModel contains the application model, feature behaviour and data boundaries. The test hierarchy follows the same division.

Feature-based architecture is not achieved by moving a thousand-line object into a folder named Features. The folder must contain a readable capability with clear state, product verbs and dependencies. A developer repairing a habit streak should be able to open Features/Habits without touring unrelated networking, services, helpers and utility folders.

04 · TUTORIAL

Declarative SwiftUI with Tightly Coupled ViewModels

Trend uses SwiftUI as a presentation system. A View declares what should appear for the current state and sends user intent to its ViewModel.

CODE EXAMPLE

struct TodayView: View {
    @State private var viewModel = TodayViewModel()

    var body: some View {
        NavigationStack {
            if case .failed(let message) = viewModel.loadState {
                loadFailure(message)
            } else {
                entryContent
            }
        }
        .task(id: viewModel.loadState) {
            guard viewModel.loadState == .ready else { return }
            await focusWeightField()
        }
    }
}

A substantial screen and its ViewModel are deliberately tightly coupled. TodayViewModel exists to make TodayView simple. It can expose presentation state, format screen-specific values, own an edit draft and translate button taps into feature operations. That coupling is readable and local.

The coupling stops at the product boundary. A ViewModel must not become a second application model that reimplements validation, persistence, projections or synchronization. Reusable visual components may need no ViewModel at all; a unique screen with several states and actions usually benefits from one.

CODE EXAMPLE

@MainActor
@Observable
final class TodayViewModel {
    private let today: WeightEntryManager

    var draft: WeightEntryDraft
    var errorMessage: String?
    private(set) var isSaving = false

    init(today: WeightEntryManager = AppModel.shared.weightEntries) {
        self.today = today
        draft = today.makeWeightEntryDraft(editing: nil)
    }

    func refresh() async {
        await today.refresh()
    }
}

@Observable makes the presentation model visible to SwiftUI. @MainActor gives its mutable UI-facing state one isolation domain. Neither attribute decides the business rules. The feature manager does.

05 · TUTORIAL

The Model Provides the Feature Manager

AppModel is Trend's composition root. It creates the live repositories, workers and managers, connects their dependencies, and provides the long-lived feature capabilities that ViewModels use.

CODE EXAMPLE

@MainActor
final class AppModel {
    static let shared = AppModel.live()

    let weightEntries: WeightEntryManager
    let progressFeature: ProgressManager
    let settingsFeature: SettingsManager
    let habitsFeature: HabitsManager
    let purchaseFeature: PurchaseManager
}

A ViewModel requests the smallest feature API it needs rather than retaining the entire application model. The default dependency makes production construction easy:

CODE EXAMPLE

init(today: WeightEntryManager = AppModel.shared.weightEntries)

A test can supply a manager assembled with an in-memory repository. The screen remains easy to create, while the real dependency stays replaceable.

The lifetime difference matters. The ViewModel belongs to a screen. The feature manager represents a product capability that may serve several screens and survive their recreation. The application model assembles that capability once and exposes it in product language.

06 · TUTORIAL

A Feature Manager Provides a Readable API

A feature manager should read like the feature, not like a bag of technical operations. Trend's WeightEntryManager exposes verbs a product developer can understand:

CODE EXAMPLE

func makeWeightEntryDraft(editing entry: WeightEntry?) -> WeightEntryDraft
func checkIn(_ draft: WeightEntryDraft) async throws -> DailyCheckInResult
func save(_ draft: WeightEntryDraft, editing entry: WeightEntry?) async throws
func delete(_ entry: WeightEntry) async throws
func refresh() async

The caller asks to check in, save, delete or refresh. It does not orchestrate a repository write, a progress recalculation, a streak update and a tip selection from the View. The manager coordinates those collaborators and publishes the resulting feature state.

We call these managers cooperative for two reasons. First, they cooperate with related features instead of allowing presentation code to coordinate the application. Second, their asynchronous work cooperates with Swift's concurrency model: cancellation is observed, task lifetime has an owner, and values cross isolation boundaries deliberately.

Cooperative feature manager

A product-facing type that owns a feature's observable state, workflows and task lifetime while coordinating narrower repositories, actors or neighbouring feature capabilities.

07 · TUTORIAL

No Business Logic in the UI

Business logic must be extracted from the UI and placed in the testable application model or feature. This is the most important maintenance rule in the architecture.

A View may decide presentation details: spacing, colour, focus, animation, sheet visibility and how a value is displayed. It must not decide whether a weight is valid, how a streak changes, which purchase unlocks a feature, whether a record should replace another record, how a projection is calculated or when persistence is considered successful.

The ViewModel may adapt feature state for one screen and manage temporary presentation input. It must not become the hidden home of reusable product rules merely because the View can call it conveniently.

A practical test is to imagine adding a widget, watch app or second screen. If that client must copy logic from a SwiftUI View or its screen-specific ViewModel, the business rule is in the wrong place. If it can call a feature API and receive the same behaviour, the boundary is working.

This rule also protects the refresh rate. SwiftUI's body can be evaluated frequently. It should describe inexpensive presentation, not perform storage, networking, large transformations or accidental work whose lifetime changes with rendering.

08 · TUTORIAL

Worker Actors Provide Deliberate Isolation

A worker actor is useful when part of a feature owns mutable state that may be approached concurrently, or when the application needs a clear isolation boundary beneath its UI-facing manager.

Trend's habits feature illustrates the relationship:

EXECUTION DIAGRAM

MainActor
HabitsView → HabitsViewModel → HabitsManager
                                   ↓ await
                              HabitsWorker actor
                                   ↓
                              HabitDataStore

HabitsManager owns observable UI-facing state and manager-owned tasks on MainActor. Its private HabitsWorker actor owns access to the habit data store and prepares summaries behind an isolation boundary. The View never talks to the worker directly.

Trend also uses a ProgressInsights actor to prepare sendable history data. Inputs cross into that actor and a result returns. The UI-facing manager then publishes the result on MainActor.

Do not add an actor to every feature as decoration. An actor solves an isolation problem; it does not automatically make work faster, move every operation to a background thread or define product correctness. If the feature only owns lightweight UI-facing state, a @MainActor manager may be the simpler and better design.

09 · TUTORIAL

Concurrency Is Designed from the Beginning

Modern asynchronous architecture decides ownership before writing Task { }.

For each asynchronous operation, the feature should answer:

• Which type owns the task?

• Which actor owns the mutable state?

• What values cross the isolation boundary?

• Who requests cancellation?

• How does the operation observe cancellation?

• May an older result replace a newer one?

• What state is published after failure?

async does not mean “off the main thread.” await marks a possible suspension point; it does not make long synchronous work cheap. UI-facing state belongs naturally on MainActor, while an actor or another explicit concurrent boundary can own independent work where the product requires it.

Cancellation is cooperative. A cancelled task must reach an operation that observes cancellation or check it explicitly. Actor isolation prevents simultaneous access to actor-isolated state, but it does not decide whether a stale result is still meaningful. The feature must define rules such as latest request wins, first result wins or all results merge.

Keep the UI free

Suspending while data is unavailable protects responsiveness. Moving substantial synchronous preparation behind a deliberate isolation boundary protects it further. Measuring the result proves whether the design actually maintains a smooth interface.

10 · TUTORIAL

KISS

Keep It Simple. Modern architecture should reduce the number of paths through the application.

Do not add a coordinator because architecture articles mention coordinators. Do not create a protocol for every concrete type. Do not wrap a repository in a service, then a use case, then an interactor, when the feature manager can express the operation directly. Do not split a small application into packages merely to make the dependency graph look impressive.

Every abstraction must earn its existence by doing at least one useful job: clarifying a product capability, enforcing ownership, replacing an external system in tests, protecting isolated state or allowing a genuinely independent module to evolve.

KISS does not mean putting everything in one file. It means having the fewest concepts that preserve the required boundaries. In Trend, a typical route is intentionally short:

EXECUTION DIAGRAM

TodayView → TodayViewModel → WeightEntryManager → WeightRepository

A worker actor appears only when it solves a real concurrency or isolation problem. The architecture grows in response to product pressure, not imagination.

11 · TUTORIAL

Maintainability Over Bloat

AI can produce code much faster than a human can read it. That asymmetry creates a new architectural risk: the application can accumulate alternative code paths, adapters, generic helpers, duplicated states, unnecessary protocols and speculative layers before the team understands what was added.

The answer is not to stop using AI. The answer is to make deletion and simplification part of the development loop.

1. Generate the smallest working version of the feature.

2. Build it and exercise the real behaviour in Xcode.

3. Read every changed file.

4. Ask the coding assistant to identify duplicated paths and abstractions with only one implementation.

5. Remove code that does not protect a product requirement.

6. Build and test again after the reduction.

For iOS product work, pair programming with an AI assistant inside Xcode gives the most useful feedback loop: the project, compiler errors, previews, tests, simulator and device are beside the conversation. Terminal tools remain valuable for source control and automation, but they cannot replace seeing and touching the feature as an application.

A codebase generated without this reduction pass tends to become larger and harder to explain. A codebase repeatedly generated, reviewed, simplified and tested can become smaller even while the product gains behaviour.

12 · TUTORIAL

True Rapid Prototyping

We can now prototype at the level of a real application rather than painting a disposable screen in an empty project.

A highly defined architecture gives an AI assistant rails. We can say: add the product behaviour to the feature manager, isolate mutable worker state in an actor, expose only the presentation required by the ViewModel, render it in SwiftUI and test the observable result. The assistant does not have to invent a new architecture for every prompt.

This lets one developer explore changes that previously demanded much more time and coordination. A large first draft, a repaired test area or even a broad application reshape may sometimes be produced in minutes or an hour. Those are possible iteration speeds, not promises that every result is production-ready. The important change is that ambitious experiments have become cheap enough to try.

Rapid prototyping now includes real persistence, real concurrency, real feature interactions and a build running on an iPhone. We direct both the architectural boundary and the individual code, then test what the feature actually does.

13 · TUTORIAL

Ignoring AI Is Foolish

AI coding assistance is now part of professional software development. It can inspect a project, create a feature draft, generate tests, explain an unfamiliar path, repair compiler failures and help reshape code across several files. Apple itself is integrating coding agents into Xcode's project, build, documentation and testing workflows.

Refusing to use that capability does not preserve quality. It preserves the old cost of producing and exploring software. A developer who combines product knowledge, architectural direction and AI-assisted implementation can attempt more ideas, compare more designs and test more failure paths than the same developer could manually type in the same time.

The correct response is disciplined adoption: give the assistant an explicit architecture, constrain the change, inspect the diff, build the product, run the tests and remove everything that does not earn its place.

14 · TUTORIAL

What We Lose with AI

We lose some automatic knowledge of how the code works.

When we type every line, we build a slow, incidental memory of the implementation. When AI creates several coordinated files in seconds, the output can be larger and more professional than what we would have produced alone, but our understanding does not grow at the same speed.

We must admit that trade-off instead of pretending that reviewing generated code is identical to authoring it. The repair is deliberate comprehension:

• ask the assistant to explain the complete runtime path;

• trace one user action from View to repository and back;

• review every new task, actor and mutable state owner;

• remove code that nobody on the team can justify;

• and make the tests state the behaviour in product language.

We are not building rockets that fly into outer space. We are building iOS applications, and many existing applications already contain crashes, hitches and incorrect edge cases. AI does not introduce the existence of bugs. It changes how quickly we can create, expose and repair them.

That is not permission to ship carelessly. It is permission to explore fearlessly on a branch, then apply strict release safeguards. The previous process could take months and still produce a buggy application at enormous cost. The new process can produce several testable alternatives quickly. Human judgement decides which one deserves to ship.

15 · TUTORIAL

Git Makes Experiments Cheap

Git turns rapid generation into a reversible experiment.

1. Create a branch for one product hypothesis.

2. Ask the assistant to implement it within the established feature boundaries.

3. Inspect the diff and remove generated bloat.

4. Build the branch and run it on an iPhone.

5. Show the team the behaviour, not a diagram promising future behaviour.

6. Merge the experiment, revise it or delete the branch.

A prototype can be ready to discuss while the team gets a coffee. The exact time will vary, but the economic decision has changed: testing an idea in the product may now be cheaper than holding a long meeting about whether the idea is technically possible.

We should loosen rigidity around how much code may be explored. We should not loosen release discipline. Branches, code review, compiler checks, automated tests, performance measurements, staged releases and human testing replace fear of change with evidence.

16 · TUTORIAL

Real Testing

A test is valuable when it protects behaviour. Writing a test merely to increase the number of tests by one is accounting, not engineering.

The architecture makes real testing easier because business behaviour has been removed from SwiftUI. A feature manager can be assembled with an in-memory repository and exercised without tapping through the application. Its tests can ask product questions:

• Does a valid check-in persist before new state is published?

• Does an invalid entry leave stored history unchanged?

• Does a stale asynchronous result fail to replace a newer result?

• Does cancellation retire work the feature no longer owns?

• Does a synchronization failure preserve recoverable local data?

• Do repeated calls share or replace work according to the documented rule?

AI gives us enormous leverage here. We can ask it to enumerate success, failure, cancellation and adverse-completion paths for each feature; create the in-memory dependencies; generate the first test suite; run it; and repair the implementation or test assumptions. The human still decides which behaviours matter and whether each assertion proves them.

Tests should sit beside architectural work, not arrive after the implementation as coverage decoration. A feature is not understood until its normal behaviour, failure behaviour and concurrency policy can be stated clearly enough to test.

17 · TUTORIAL

AI UI Automation Testing

Unit tests prove feature behaviour beneath the interface. UI automation proves that a user can reach and observe that behaviour through the assembled application.

AI can help build an XCUITest suite for critical journeys: launch Trend, add a weight, edit it, change units, create a habit, relaunch the app and confirm persistence. Xcode can record interactions into test code, replay UI automation and attach screenshots, video and diagnostics to test reports. Current Xcode tooling also supports performance measurements such as animation-hitch metrics.

A strong UI automation workflow is:

1. Give important controls stable accessibility identifiers.

2. Record or describe one real user journey.

3. Ask the coding assistant to convert repetition into readable test helpers.

4. Add assertions for visible product outcomes, not implementation details.

5. Replay the journey on relevant devices, orientations, locales and accessibility settings.

6. Keep screenshots and diagnostics with failed release-candidate runs.

Generated UI tests still require review. An assistant can write a test that always passes, waits for the wrong element or verifies only that a button exists. The purpose is confidence in a customer journey, not another green mark.

18 · TUTORIAL

Human Testers Remain Mandatory

Automated tests execute the paths we select and assert the outcomes we describe. They can be skipped, disabled, weakened or simply wrong. They cannot fully judge whether a transition feels jerky, a workflow is confusing, a message is trustworthy or the application behaves well in the untidy conditions of real life.

Human testers therefore remain a mandatory release boundary. They should explore the release candidate on physical devices, interrupt operations, change connectivity, enter surprising data, use accessibility features and look for behaviour the suite never imagined.

AI increases the number of paths we can automate. Humans decide whether the product is actually ready.

19 · TUTORIAL

What Modern iOS Architecture Means in Practice

For Trend, the answer is now concrete:

1. SwiftUI renders state and sends intent.

2. A tightly coupled ViewModel serves one substantial screen.

3. AppModel assembles and provides long-lived feature managers.

4. A cooperative feature manager owns business behaviour, observable feature state and asynchronous policy.

5. A repository defines the boundary to persistence or another external system.

6. A worker actor is added when independent state or concurrent work requires isolation.

7. Tests protect real feature behaviour, failure paths and concurrency rules.

8. AI and Git make broad experiments fast and reversible.

9. Human review and testing remain the release authority.

Modern iOS architecture

SwiftUI MVVM with cooperative feature managers and worker actors: a product-shaped, concurrency-aware structure designed for rapid experimentation without surrendering maintainability.

The architecture is successful when a developer can locate a feature, read what it does, test its behaviour, change it without surprising unrelated screens and remove generated code that no longer serves the product.

That is the standard we will continue testing in the open-source Trend iOS app. The repository is the experiment, the implementation is the evidence, and the shipped behaviour—not the diagram—is the final judge.

20 · TUTORIAL

Frequently Asked Questions

Is this architecture simply MVVM?

No. MVVM describes the presentation boundary between a SwiftUI screen and its ViewModel. Feature managers, repositories, actor isolation, task ownership and result-ordering policies define the application beneath that boundary.

Does every screen need a ViewModel?

No. A small reusable visual component may render its inputs directly. A substantial product screen benefits from a tightly coupled ViewModel when it owns presentation state, user input or several feature interactions.

Does every feature need a worker actor?

No. Add a worker actor only when the feature has independent mutable state or work that needs a deliberate isolation boundary. UI-facing observable state can remain in a @MainActor feature manager.

Should business logic ever live in a SwiftUI View?

No reusable product rule should live there. Views may contain presentation decisions, but validation, persistence, calculations, synchronization and feature policy belong in the model or feature layer.

Can AI-generated code be production quality?

It can contribute to production-quality software, but generation is only the first draft. The team must inspect the changes, simplify the design, build the application, test real behaviour, measure performance and perform human release testing.

21 · TUTORIAL

Continue Learning

Next, examine one boundary in detail: How Should a Modern iOS Feature Be Structured? We will follow a Trend feature from its SwiftUI screen through its ViewModel, manager, worker and repository, including its task ownership and tests.

bottom of page