DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Large-Scale App Composition (100+ Screens, Real Architecture)

Most SwiftUI examples are:

  • 3 screens
  • 1 ViewModel
  • 0 architecture
  • 100% optimism

Real products are:

  • 50–200+ screens
  • multiple teams
  • parallel features
  • constant refactors
  • continuous releases

If your architecture isn’t designed for scale, it will collapse under its own weight.

This post shows how to structure large SwiftUI apps so they stay:

  • navigable
  • testable
  • modular
  • fast to build
  • safe to change

This is not theory — this is how big apps actually survive.


🧠 The Core Principle

Features scale.

Files don’t.

If your app structure is based on file type:
Views/
ViewModels/
Services/

You will suffer.

Large apps are composed by features, not layers.


🧱 1. Feature-First Folder Structure

Features/
Home/
HomeView.swift
HomeViewModel.swift
HomeContainer.swift
Profile/
ProfileView.swift
ProfileViewModel.swift
ProfileContainer.swift
Settings/
SettingsView.swift
SettingsViewModel.swift
SettingsContainer.swift

Each feature owns:

  • its UI
  • its state
  • its dependencies
  • its logic

No cross-feature imports.


📦 2. Feature Containers (Again, Because This Is Everything)

Every feature has a container:

final class ProfileContainer {
    let viewModel: ProfileViewModel

    init(app: AppContainer) {
        self.viewModel = ProfileViewModel(
            repo: app.profileRepository,
            analytics: app.analytics
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

This gives you:

  • explicit ownership
  • clean teardown
  • no hidden dependencies
  • easy testing

🧭 3. Central Routing Without Central Coupling

Never do:

NavigationLink(destination: ProfileView())
Enter fullscreen mode Exit fullscreen mode

Instead:

enum Route {
    case profile(id: String)
    case settings
}
Enter fullscreen mode Exit fullscreen mode
func build(route: Route) -> some View {
    switch route {
    case .profile(let id):
        ProfileFeature(id: id)
    case .settings:
        SettingsFeature()
    }
}
Enter fullscreen mode Exit fullscreen mode

Routing is data, not UI.


🧩 4. Feature Registration Pattern

Each feature exposes a factory:

protocol Feature {
    func build() -> AnyView
}
Enter fullscreen mode Exit fullscreen mode
struct ProfileFeature: Feature {
    func build() -> AnyView {
        AnyView(ProfileView())
    }
}
Enter fullscreen mode Exit fullscreen mode

Your router knows only:

  • feature type
  • route
  • not internals

This is how you avoid god routers.


🧠 5. Build Times Matter at Scale

When you hit 100+ screens:

  • compile times explode
  • previews slow down
  • Swift type checker cries

Solutions:

  • split into Swift Packages
  • isolate features
  • reduce cross-module imports
  • avoid giant view bodies
  • use small composable views

Architecture affects build time.


🧬 6. Dependency Direction Rules

Dependencies should flow:

App
  Feature
    Subview
Enter fullscreen mode Exit fullscreen mode

Never:

Feature A  Feature B
Enter fullscreen mode Exit fullscreen mode

If Feature A needs Feature B:

  • extract shared logic
  • move to a shared module
  • never import features into features

This rule prevents circular hell.


🧪 7. Testing at Scale

Because features are isolated:

let container = ProfileContainer(app: mockApp)
let vm = container.viewModel
Enter fullscreen mode Exit fullscreen mode

You can test:

  • features independently
  • without app boot
  • without navigation
  • without globals

This is impossible with layer-based architecture.


🪜 8. Gradual Refactors Without Fear

Feature isolation allows:

  • deleting features safely
  • rewriting features independently
  • migrating architecture gradually
  • parallel team work

This is how large apps evolve.


⚠️ 9. Avoid the “Shared” Folder Trap

Shared/
Utils/
Helpers/
Common/
Enter fullscreen mode Exit fullscreen mode

This becomes:

The junk drawer of shame.

Instead:

  • name modules explicitly
  • define ownership
  • enforce boundaries
  • delete aggressively

Shared code should be rare and intentional.


❌ 10. Common Scaling Anti-Patterns

Avoid:

  • god ViewModels
  • giant routers
  • global singletons
  • cross-feature imports
  • shared mutable state
  • “just one more helper”
  • dumping everything in AppState

These kill scale.


🧠 Mental Model

Think:

App = composition of features
Feature = composition of views + logic
View = pure function of state
Enter fullscreen mode Exit fullscreen mode

Never:

“The app is one big thing”

It is many small, replaceable things.


🚀 Final Thoughts

Large SwiftUI apps don’t fail because of SwiftUI.

They fail because of:

  • poor composition
  • unclear ownership
  • weak boundaries
  • architectural shortcuts

When features are isolated and composed cleanly:

  • teams move faster
  • bugs decrease
  • refactors are safe
  • code stays readable
  • scaling is natural

Top comments (0)