# Dependency Injection in SwiftUI: Composing Views Without Singletons

One of the most common pitfalls in SwiftUI apps is the overuse of singletons and global state. It feels convenient at first — you just reach for `UserDefaults.standard`, a shared `NetworkService.shared`, or a global `@EnvironmentObject` — but it quietly makes your code untestable, hard to reason about, and tightly coupled.

This article shows how to apply **Dependency Injection (DI)** in SwiftUI to keep your views composable, your ViewModels testable, and your architecture clean.

* * *

## The Problem: Hidden Dependencies

Let's say you're building a simple favorites feature. A common approach looks like this:

```swift
class FavoritesViewModel: ObservableObject {
    @Published var favorites: [Item] = []

    func loadFavorites() {
        // Directly coupled to a concrete singleton
        favorites = FavoritesStore.shared.getAll()
    }
}
```

And the view:

```swift
struct FavoritesView: View {
    @StateObject private var viewModel = FavoritesViewModel()

    var body: some View {
        List(viewModel.favorites) { item in
            Text(item.name)
        }
        .onAppear { viewModel.loadFavorites() }
    }
}
```

This works at runtime. But try writing a unit test for `FavoritesViewModel`:

*   You can't control what `FavoritesStore.shared` returns.
    
*   Tests become dependent on shared mutable state.
    
*   Running tests in parallel risks interference between test cases.
    

The dependency on `FavoritesStore.shared` is **hidden inside the ViewModel**. Hidden dependencies are a form of technical debt that compounds over time.

* * *

## The Solution: Inject Dependencies Through the Initializer

The fix is to **invert the dependency**: instead of the ViewModel reaching out for what it needs, we *give* it what it needs.

Start by defining a protocol that abstracts the store:

```swift
protocol FavoritesStoreProtocol {
    func getAll() -> [Item]
}
```

Make the real store conform to it:

```swift
class FavoritesStore: FavoritesStoreProtocol {
    static let shared = FavoritesStore()

    func getAll() -> [Item] {
        // real implementation
    }
}
```

Now inject the dependency into the ViewModel:

```swift
class FavoritesViewModel: ObservableObject {
    @Published var favorites: [Item] = []

    private let store: FavoritesStoreProtocol

    init(store: FavoritesStoreProtocol) {
        self.store = store
    }

    func loadFavorites() {
        favorites = store.getAll()
    }
}
```

The ViewModel no longer knows (or cares) *how* favorites are retrieved. It only knows *what* to call.

* * *

## Making the View Composable

With DI in place, the view needs to receive a configured ViewModel:

```swift
struct FavoritesView: View {
    @StateObject private var viewModel: FavoritesViewModel

    init(viewModel: FavoritesViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel)
    }

    var body: some View {
        List(viewModel.favorites) { item in
            Text(item.name)
        }
        .onAppear { viewModel.loadFavorites() }
    }
}
```

The **Composition Root** — where you wire everything together — lives at the entry point of your app:

```swift
@main
struct FavoritesApp: App {
    var body: some Scene {
        WindowGroup {
            FavoritesView(
                viewModel: FavoritesViewModel(store: FavoritesStore.shared)
            )
        }
    }
}
```

Notice that `FavoritesStore.shared` appears only once, at the top. The rest of the codebase never references it directly.

* * *

## Writing Tests Without Infrastructure

Now you can test the ViewModel in complete isolation with a stub:

```swift
class FavoritesStoreStub: FavoritesStoreProtocol {
    var stubbedFavorites: [Item] = []

    func getAll() -> [Item] {
        stubbedFavorites
    }
}
```

```swift
final class FavoritesViewModelTests: XCTestCase {

    func test_loadFavorites_populatesListWithStoredItems() {
        let store = FavoritesStoreStub()
        store.stubbedFavorites = [Item(name: "SwiftUI"), Item(name: "TDD")]
        let sut = FavoritesViewModel(store: store)

        sut.loadFavorites()

        XCTAssertEqual(sut.favorites.map(\.name), ["SwiftUI", "TDD"])
    }

    func test_loadFavorites_withEmptyStore_producesEmptyList() {
        let store = FavoritesStoreStub()
        let sut = FavoritesViewModel(store: store)

        sut.loadFavorites()

        XCTAssertTrue(sut.favorites.isEmpty)
    }
}
```

These tests are:

*   **Fast** — no network, no disk, no shared state.
    
*   **Deterministic** — the stub always returns what you configure.
    
*   **Isolated** — each test controls its own world.
    

* * *

## Passing Dependencies Through the View Hierarchy

For deeper view hierarchies, you have two clean options.

### Option 1: Explicit Passing (Preferred for Testability)

Pass dependencies through initializers at every level. It's verbose, but the dependency graph is explicit and auditable.

```swift
struct RootView: View {
    let store: FavoritesStoreProtocol

    var body: some View {
        NavigationStack {
            FavoritesView(
                viewModel: FavoritesViewModel(store: store)
            )
        }
    }
}
```

### Option 2: `@EnvironmentObject` with a Protocol Wrapper

When explicit passing becomes impractical (e.g. very deep hierarchies), `@EnvironmentObject` is acceptable — but inject at the Composition Root, not a singleton:

```swift
// Define a concrete container
class AppDependencies: ObservableObject {
    let store: FavoritesStoreProtocol

    init(store: FavoritesStoreProtocol) {
        self.store = store
    }
}

// Inject once at the root
@main
struct FavoritesApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
                .environmentObject(AppDependencies(store: FavoritesStore.shared))
        }
    }
}
```

The key difference from a singleton: you control what goes into `AppDependencies`. In tests, you inject a stub version. No global mutable state.

* * *

## Previews for Free

A nice side effect of DI: Xcode Previews become trivial to configure.

```swift
#Preview {
    let store = FavoritesStoreStub()
    store.stubbedFavorites = [
        Item(name: "Dependency Injection"),
        Item(name: "TDD"),
        Item(name: "Clean Architecture")
    ]
    return FavoritesView(viewModel: FavoritesViewModel(store: store))
}
```

No more fighting Previews because they hit the real network or real database.

* * *

## What You Gain

| Before (Singleton) | After (DI) |
| --- | --- |
| Hidden dependencies | Explicit, visible dependencies |
| Hard to test | Fully unit-testable |
| Shared mutable state in tests | Each test controls its own world |
| Previews require real infrastructure | Previews use stubs |
| Tight coupling | Views and ViewModels are composable |

* * *

## Summary

Dependency Injection is not a complex pattern — it's simply the discipline of **giving a component what it needs rather than letting it reach out for it**. In SwiftUI:

1.  Define protocols for your dependencies.
    
2.  Inject concrete implementations through initializers.
    
3.  Wire everything at the Composition Root (your `@main` struct).
    
4.  Use stubs in tests and previews.
    

This one practice unlocks testability, composability, and clear architecture — all at once. It's the foundation on which Clean Architecture, the Decorator pattern, and every other technique rely on.
