DEV Community

Sebastien Lato
Sebastien Lato

Posted on

Modular Feature Architecture in SwiftUI

As SwiftUI apps grow, one problem always appears:

Everything ends up in one giant project.

Features depend on each other.

Services become messy.

Navigation gets tangled.

Previews slow down.

Build times increase.

Code ownership becomes unclear.

A scalable SwiftUI codebase requires feature modules β€” isolated pieces of your app that own their:

  • UI
  • ViewModels
  • services
  • models
  • routing
  • previews

This is how real production apps stay maintainable.

This post gives you the complete modular architecture pattern you can adopt in any SwiftUI app.


🧩 1. What Is a Feature Module?

A feature module is a self-contained unit representing one functional chunk of your app:

Home/
Profile/
Settings/
Feed/
Auth/
OfflineSync/
Notifications/
Enter fullscreen mode Exit fullscreen mode

Each module contains:

  • Views
  • ViewModels
  • Models
  • Services
  • Routing definitions
  • Previews
  • Mocks

A feature should be removable without breaking the app.

If you can delete a folder and nothing else breaks β†’ it is modular.


πŸ“ 2. Recommended Folder Structure

Here is the clean, scalable pattern:

AppName/
β”‚
β”œβ”€β”€ App/
β”‚ β”œβ”€β”€ AppState.swift
β”‚ β”œβ”€β”€ AppEntry.swift
β”‚ └── RootView.swift
β”‚
β”œβ”€β”€ Modules/
β”‚ β”œβ”€β”€ Home/
β”‚ β”‚ β”œβ”€β”€ HomeView.swift
β”‚ β”‚ β”œβ”€β”€ HomeViewModel.swift
β”‚ β”‚ β”œβ”€β”€ HomeService.swift
β”‚ β”‚ β”œβ”€β”€ HomeRoute.swift
β”‚ β”‚ └── HomeMocks.swift
β”‚ β”‚
β”‚ β”œβ”€β”€ Profile/
β”‚ β”‚ β”œβ”€β”€ ProfileView.swift
β”‚ β”‚ β”œβ”€β”€ ProfileViewModel.swift
β”‚ β”‚ β”œβ”€β”€ ProfileService.swift
β”‚ β”‚ β”œβ”€β”€ ProfileRoute.swift
β”‚ β”‚ └── ProfileMocks.swift
β”‚ β”‚
β”‚ β”œβ”€β”€ Settings/
β”‚ β”‚ β”œβ”€β”€ SettingsView.swift
β”‚ β”‚ β”œβ”€β”€ SettingsViewModel.swift
β”‚ β”‚ └── SettingsMocks.swift
β”‚ β”‚
β”‚ └── Shared/
β”‚ β”œβ”€β”€ Components/
β”‚ β”œβ”€β”€ Models/
β”‚ β”œβ”€β”€ Utilities/
β”‚ └── Styles/
β”‚
β”œβ”€β”€ Services/
β”‚
└── Resources/
Enter fullscreen mode Exit fullscreen mode

No more β€œGod folders”.

Each module is its own world.


πŸ”Œ 3. Dependency Injection at the Module Boundary

Each module declares what it needs via protocols:

protocol ProfileServiceProtocol {
    func fetchProfile(id: String) async throws -> Profile
}
Enter fullscreen mode Exit fullscreen mode

The module does not know about:

  • real API client
  • offline cache
  • mock data source
  • testing environment

The app injects the concrete service:

ProfileViewModel(
    service: appServices.profileService
)
Enter fullscreen mode Exit fullscreen mode

This creates:

  • testability
  • flexibility
  • replacement
  • isolation

🧭 4. Routing Between Modules

Each module defines its own route type:

enum ProfileRoute: Hashable {
    case details(id: String)
    case followers(id: String)
}
Enter fullscreen mode Exit fullscreen mode

RootView aggregates module routes:

enum AppRoute: Hashable {
    case profile(ProfileRoute)
    case home
    case settings
}
Enter fullscreen mode Exit fullscreen mode

And handles navigation centrally:

.navigationDestination(for: AppRoute.self) { route in
    switch route {
    case .profile(let pr): ProfileRouter.view(for: pr)
    case .home: HomeView()
    case .settings: SettingsView()
    }
}
Enter fullscreen mode Exit fullscreen mode

Each module also has a router:

struct ProfileRouter {
    @ViewBuilder static func view(for route: ProfileRoute) -> some View {
        switch route {
        case .details(let id):
            ProfileView(userID: id)
        case .followers(let id):
            FollowersView(userID: id)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Modules stay independent.


πŸ§ͺ 5. Feature Modules Should Be Fully Previewable

Example:

#Preview("Profile Details") {
    ProfileView(
        viewModel: ProfileViewModel(
            service: MockProfileService()
        )
    )
}
Enter fullscreen mode Exit fullscreen mode

Modules with self-contained previews:

  • speed up development
  • prevent regressions
  • reduce cognitive load

The previews folder becomes a mini design system per module.


🏎 6. Performance Wins From Modularity

A modular codebase benefits from:

βœ” Faster build times
Only changed modules recompile.

βœ” Safer refactoring
Modules don’t leak internal details.

βœ” Better test isolation
Run tests module-by-module.

βœ” Smaller mental load
New contributors understand features quickly.

βœ” Better CI parallelization
Each module becomes a test target.

This pays off massively in apps with 10+ screens.


πŸ”„ 7. Feature Communication Patterns

Modules should not import each other.

Use one of these patterns instead:

  1. AppRoute (most common)
    Root coordinates navigation.

  2. Services layer
    Modules communicate through shared service protocols.

  3. Event bus
    Only for global side effects like analytics.

  4. Shared models
    Placed explicitly under /Modules/Shared.

The key rule:
πŸ“Œ Modules talk β€œupward”, not sideways.


🧡 8. Isolate Business Logic Inside Each Module

All module logic belongs in the module:

Profile/
   ProfileViewModel.swift
   ProfileService.swift
   ProfileValidation.swift
   ProfileFormatter.swift
Enter fullscreen mode Exit fullscreen mode

Avoid:

  • global utils
  • shared state
  • importing unrelated modules

This keeps code ownership clean.


🧱 9. Shared Module for Cross-App Pieces

Your /Modules/Shared folder should contain:

  • base components
  • typography
  • theme system
  • global models
  • networking utilities
  • animation helpers

But never feature-specific logic.


🧭 10. When Should You Modularize?

Use this checklist:

  • App has 6+ screens
  • Two developers or more
  • Features ship independently
  • You need fast previews
  • You need safe refactors
  • You use deep linking
  • You support offline mode
  • You use DI + global AppState

If you checked 3 or more β†’ modularize.


πŸš€ Final Thoughts

Modularizing a SwiftUI app transforms your codebase:

  • easier to work on
  • easier to test
  • easier to scale
  • easier to refactor
  • easier to collaborate on

This is one of the biggest upgrades you can make to your architecture.

Top comments (0)