Post

Dependency Injection in SwiftUI Previews: A Modern Approach

Discover how dependency injection keeps SwiftUI previews maintainable and easy to test.

Dependency Injection in SwiftUI Previews: A Modern Approach

In the fast-paced world of iOS development, previews are a powerful tool that allows us to iterate quickly and see our UI changes in real time. However, as our applications grow more complex, so too do the dependencies that our views rely on. Without a proper strategy, managing these dependencies in SwiftUI previews can become cumbersome and error-prone.

In this blog post, we will explore how to effectively use dependency injection within SwiftUI previews. We’ll discuss the benefits of this approach, how to set up a dependency container, and how to create a PreviewWrapper that injects dependencies seamlessly into your views. By the end of this article, you’ll have a robust and scalable solution for managing dependencies in your SwiftUI previews.

Why Dependency Injection Matters in Previews

Before diving into the implementation, let’s briefly discuss why dependency injection is important, particularly in the context of SwiftUI previews.

The Problem with Hard-Coded Dependencies

In many iOS applications, views depend on various services, such as network clients, databases, or analytics providers. In production, these services are configured to work with live data and systems. However, in a preview environment, using live services is not only unnecessary but also potentially problematic. For instance, you don’t want your previews making real network requests or persisting data to a live database.

Hard-coding dependencies within your views leads to several issues:

  • Difficult to Test: Each preview may require you to mock dependencies manually.
  • Brittle Code: Any changes to dependencies could require significant refactoring.
  • Slow Iteration: Preview performance can suffer if live dependencies are used.

The Solution: Dependency Injection

Dependency injection (DI) solves these problems by allowing you to inject the required dependencies into your views from the outside. This makes your views more modular, testable, and flexible. When combined with a well-designed dependency container, DI enables you to easily switch between live and preview environments, ensuring that your previews run smoothly and independently of your production code.

Setting Up the Dependency Container

The core of our solution lies in the iOSDependencyContainer, a class that provides all necessary dependencies for our app. This container is responsible for managing different configurations for live and preview environments. By using this container, we can easily switch between live services and mock services, making our previews both fast and accurate representations of our app.

What is iOSDependencyContainer?

The iOSDependencyContainer is a central place where all the dependencies for your app are created and managed. This includes services like network clients, database clients, or any other objects your views and models depend on.

Here’s a simplified version of what the iOSDependencyContainer might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Foundation

public class iOSDependencyContainer {

    // Example services that might be used across the app
    let networkClient: NetworkClient
    let databaseClient: DatabaseClient

    // Initialize the container with an option to use in-memory data for previews
    public init(inMemory: Bool) {
        if inMemory {
            // Use mock or in-memory implementations for previews
            self.networkClient = MockNetworkClient()
            self.databaseClient = InMemoryDatabaseClient()
        } else {
            // Use real implementations for the live app
            self.networkClient = RealNetworkClient()
            self.databaseClient = PersistentDatabaseClient()
        }
    }

    // Method to inject or configure modules as needed
    public func injectModules() {
        // You can configure or inject additional dependencies here if needed
    }
}

Breaking Down the iOSDependencyContainer

  • networkClient: This could be a class that handles all network requests. In a live environment, you would use a real implementation that makes HTTP requests. In a preview environment, you might use a mock implementation that returns sample data.

  • databaseClient: Similarly, this could be a class that interacts with a local database. For live builds, you might use a persistent database client (e.g., CoreData or Realm). For previews, you can use an in-memory database that doesn’t persist data between sessions.

  • inMemory Parameter: The inMemory boolean parameter allows the container to decide whether to use real or mock implementations. When inMemory is true, the container provides mock services that are ideal for previews, ensuring that your previews are fast and don’t depend on external factors like network connectivity.

Setting Up the PreviewHelper

Now that we understand the basics of iOSDependencyContainer, let’s see how PreviewHelper uses it to inject the right dependencies based on the environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import Foundation

public class PreviewHelper {
    public static let shared = PreviewHelper(env: .preview)

    public enum EnvironmentType {
        case preview
        case live
    }

    private let iOSDependencyContainer: iOSDependencyContainer

    private init(env: EnvironmentType) {
        switch env {
        case .preview:
            iOSDependencyContainer = .init(inMemory: true)
        case .live:
            iOSDependencyContainer = .init(inMemory: false)
        }
    }

    public var container: iOSDependencyContainer{
        return iOSDependencyContainer
    }


    public func injectDependencies() {
        iOSDependencyContainer.injectModules()
    }
}

Breaking Down the PreviewHelper

  • Environment Type: The EnvironmentType enum defines two cases: .preview and .live. This allows us to configure our dependencies differently depending on the environment.

  • iOSDependencyContainer: This container holds all the dependencies your app needs. When initialized with inMemory: true, it uses in-memory configurations suitable for previews, such as mock data or test databases.

  • Returning the Container: The container property provides access to the iOSDependencyContainer instance. By returning the container, we make it easy to access the configured dependencies throughout the app or in preview setups. This approach centralizes dependency management, ensuring consistency and reducing the risk of misconfigurations.

  • Injecting Dependencies: The injectDependencies method is called to initialize and inject all necessary modules for the selected environment.

By configuring your dependencies in this way, you can ensure that your previews remain lightweight and independent from the rest of your application.

Implementing the PreviewWrapper

Next, let’s look at how we can leverage PreviewHelper in a PreviewWrapper to automatically inject dependencies into our views during previews:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import SwiftUI

@MainActor
struct PreviewWrapper<Content: View>: View {
    let view: (iOSDependencyContainer) -> Content

    init(view: @escaping (iOSDependencyContainer) -> Content) {
        PreviewHelper.shared.injectDependencies()
        self.view = view
    }

    var body: some View {
        NavigationStack {
            view(PreviewHelper.shared.container)
        }
    }
}

How the PreviewWrapper Works

  • Dependency Injection: In the initializer, we call PreviewHelper.shared.injectDependencies() to ensure that all necessary dependencies are injected before the view is displayed.

  • Content: The view property is now a closure that takes an iOSDependencyContainer and returns the content view. This allows the view to directly access the container’s dependencies.

  • NavigationStack: We wrap the content view in a NavigationStack to provide consistent navigation behavior across all previews.

This approach abstracts away the complexity of managing dependencies, allowing you to focus on building your UI while ensuring that all necessary services are properly configured.

Using the PreviewWrapper in Your Previews

With the PreviewWrapper in place, setting up previews for your SwiftUI views becomes straightforward and powerful, especially when your views depend on various services like network clients or databases. Let’s walk through a practical example where we use a ContentView that relies on a networkClient and databaseClient.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import SwiftUI

// ContentView with dependencies
struct ContentView: View {
    let networkClient: NetworkClient
    let databaseClient: DatabaseClient

    var body: some View {
        VStack {
            Text("Network: \(networkClient.fetchData())")
            Text("Database: \(databaseClient.fetchRecord())")
        }
        .padding()
    }
}

// Preview using the PreviewWrapper
#Preview {
    PreviewWrapper { container in
        ContentView(
            networkClient: container.networkClient,
            databaseClient: container.databaseClient
        )
    }
}

Conclusion

Dependency injection in SwiftUI previews is a powerful technique that can significantly improve your development workflow. By separating concerns and managing dependencies through a dedicated container, you can modular, testable views that work seamlessly across both live and preview environments.

The PreviewWrapper and PreviewHelper presented in this article provide a clean, scalable solution for handling dependencies in SwiftUI previews. By adopting this approach, you can ensure that your previews remain fast, reliable, and independent of your production codebase.

Feel free to integrate this pattern into your own projects and see the benefits of streamlined dependency management in your SwiftUI previews!

☕ Support My Work

If you found this post helpful and want to support more content like this, you can buy me a coffee!

Your support helps me continue creating useful articles and tips for fellow developers. Thank you! 🙏

This post is licensed under CC BY 4.0 by the author.