SwiftUI Dependency Graph Architecture (Object Lifetimes & Scope)
Source: Dev.to
🧠 The Core Principle
If you don’t design object lifetimes, SwiftUI will design them for you, and you won’t like the result.
Every object must have:
- a clear owner
- a clear lifetime
- a clear scope
🧱 1. The Three Lifetimes You Must Model
Every dependency falls into one of these categories:
1. App Lifetime
- analytics
- feature flags
- auth session
- configuration
- logging
2. Feature Lifetime
- ViewModels
- repositories
- coordinators
- use cases
3. View Lifetime
- ephemeral helpers
- formatters
- local state
Mixing these lifetimes leads to leaks and bugs.
🧭 2. The Dependency Graph Layers
Think in layers:
AppContainer
↓
FeatureContainer
↓
ViewModel
↓
View
Data and ownership flow downward only; nothing should flow back up.
🏗️ 3. App Container (Root of the Graph)
final class AppContainer {
let apiClient: APIClient
let authService: AuthService
let analytics: AnalyticsService
let featureFlags: FeatureFlagService
init() {
self.apiClient = APIClient()
self.authService = AuthService()
self.analytics = AnalyticsService()
self.featureFlags = FeatureFlagService()
}
}
- Created once
- Lives for the entire app
- Injected downward
- Never recreate this instance.
📦 4. Feature Containers (Scoped Lifetimes)
Each feature builds its own graph:
final class ProfileContainer {
let repository: ProfileRepository
let viewModel: ProfileViewModel
init(app: AppContainer) {
self.repository = ProfileRepository(api: app.apiClient)
self.viewModel = ProfileViewModel(repo: repository)
}
}
- Created when the feature appears
- Destroyed when the feature disappears
- Owns its
ViewModel
This gives you clean teardown.
🧩 5. ViewModels Do NOT Build Dependencies
Bad:
class ProfileViewModel {
let api = APIClient()
}
Good:
class ProfileViewModel {
let repo: ProfileRepository
init(repo: ProfileRepository) {
self.repo = repo
}
}
ViewModels consume dependencies; they never construct them.
🧬 6. Environment as Graph Injector (Not Storage)
Pass containers via the environment, not individual services:
.environment(\.profileContainer, container)
Retrieve them in a view:
@Environment(\.profileContainer) var container
The view receives its ViewModel and other dependencies without relying on global state.
🧱 7. Lifetime = Owner
If you can’t answer “Who deallocates this?” you have a bug.
Examples of ownership:
AppContainer→ app lifetimeFeatureContainer→ navigation lifetimeViewModel→ feature lifetimeView→ frame lifetime
Ownership must be visible in code.
🔄 8. Navigation Defines Object Lifetime
Navigation is memory management, not just UI.
NavigationStack(path: $path) {
FeatureEntry()
}
When the feature leaves the stack:
- Its container deallocates
- Its
ViewModeldeallocates - Subscriptions cancel
- Tasks stop
If this doesn’t happen, the graph is wrong.
🧠 9. Avoiding Singletons (Without Losing Convenience)
Instead of:
Analytics.shared.track()
Do:
analytics.track()
where analytics is injected from the AppContainer.
You keep:
- global‑like access
- testability
- control
without true global state.
🧪 10. Testing the Graph
Because everything is injected, testing is straightforward:
let mockRepo = MockProfileRepository()
let vm = ProfileViewModel(repo: mockRepo)
No need for:
- stubbing globals
- overriding singletons
- fighting the system
Your dependency graph is your test harness.
❌ 11. Common Anti‑Patterns
Avoid:
- Building services inside ViewModels
- Global static singletons
- Injecting everything everywhere
- Feature containers that outlive navigation
- Circular dependencies
- Using environment objects as service locators
These lead to leaks, bugs, untestable code, and impossible refactors.
🧠 Mental Model
Who creates it?
Who owns it?
Who destroys it?
If you can answer all three, your graph is healthy; otherwise, you have architectural debt.
🚀 Final Thoughts
A clean dependency graph provides:
- Predictable memory usage
- Clean teardown
- Easy testing
- Safer refactors
- Faster onboarding
- Fewer production bugs
It underpins:
- Modular architecture
- Multi‑platform apps
- Large teams
- Long‑lived codebases
Most SwiftUI issues at scale are not SwiftUI problems—they are graph‑design problems.