SwiftUI Dependency Graph Architecture (Object Lifetimes & Scope)

Published: (January 9, 2026 at 06:11 PM EST)
3 min read
Source: Dev.to

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 lifetime
  • FeatureContainer → navigation lifetime
  • ViewModel → feature lifetime
  • View → 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 ViewModel deallocates
  • 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.

Back to Blog

Related posts

Read more »

SwiftUI #32: ProgressView

Overview ProgressView crea una barra de progreso. El inicializador init_:value:total: recibe una etiqueta como primer argumento. value indica el progreso actua...

SwiftUI #25: Estado (@State)

El paradigma declarativo No solo se trata de cómo organizar las vistas, sino de que cada vez que cambia el estado de la aplicación, las vistas deben actualizar...