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 (1)

Collapse
 
ullashpodder profile image
Ullash

really enjoyed the post Sebastien! you have summarised the most important points for building large scale SwiftUI apps. Following you for more 🔥