Published on

Dependency Injection In SwiftUI With @EnvironmentObject

Authors
  • avatar
    Name
    Kiet
    Twitter

Dependency injection is a powerful software design pattern that promotes loose coupling and enhances the testability and maintainability of code. Understanding and implementing it can significantly improve the quality and structure of your code. This article explores the fundamentals of dependency injection and its practical application in SwiftUI.

Caveats

Implementing dependency injection in SwiftUI without a library can be a bit cumbersome using the approach described above. It requires obtaining the instance of the dependency in the parent view rather than directly in the view where it is needed. I have extensively researched this issue, but I haven’t yet found a better solution.

While this approach works for my current project, I am open to suggestions and discussions regarding better methods for implementing dependency injection in SwiftUI. If you have any insights or alternative approaches, I am eager to listen and engage in a discussion with you about more efficient ways to achieve dependency injection in SwiftUI.

Dependencies in SwiftUI Components (Views, View Models, and Services)

In SwiftUI, we typically work with three main components: views, view models, and services. Let’s explore how dependencies can be injected into each of these components.

  1. Views: SwiftUI views represent the user interface components of your app. Dependencies injected into views can be used for data retrieval, state management, or interaction with external systems. It’s important to keep views lightweight and focused on presenting data rather than performing complex operations.

  2. View Models: View models act as intermediaries between views and data sources or services. They contain the logic and state needed for a specific view. Dependencies injected into view models can include networking services, data repositories, or other services required to fulfill the view’s responsibilities.

  3. Services: Services encapsulate specific functionalities and are typically responsible for interacting with external systems or managing data. Examples of services could include API clients, data caches, or notification managers. By injecting dependencies into services, you can abstract the underlying implementation details and improve the flexibility of your app.

Introduction

In this article, we will build a simple SwiftUI app that consists of a text field to display a number and a button to generate a new number. This app will serve as an example to showcase the concept of dependency injection and how it can be applied in SwiftUI.

View

Let’s create the view, which will display the current number and the button to generate a new number.

  var body: some View {
    VStack {
      Text(viewModel.number.description)
      Button("Generate New Number") {
        viewModel.getRandomNumber()
      }
    }
    .padding()
  }

Service

Now we will create a NumberGenerator class that will be responsible for generating random numbers. We will use this class to practice injecting dependencies.

class NumberGenerator: ObservableObject {

  func getRandomNumber() -> Int {
    return Int.random(in: 1...100)
  }
}

View Model

Next, we’ll create a view model called NumberViewModel, which will be responsible for managing the state of our app. It will hold the current number generated by the NumberGenerator and provide a method to generate a new number.

class NumberScreenViewModel: ObservableObject {
  private let numberGenerator: NumberGenerator

  init(numberGenerator: NumberGenerator) {
    self.numberGenerator = numberGenerator
  }

  @Published var number: Int = 0

  func getRandomNumber() {
    self.number = numberGenerator.getRandomNumber()
  }

}

Injecting the NumberGenerator Dependency

Initialize NumberGenerator

In the app’s entry point (App struct), create an instance of the NumberGenerator using the @StateObject property wrapper, and make it available to all children by passing it as an environment object.

@main
struct DependencyInjectionPlaygroundApp: App {
  @StateObject private var numberGenerator = NumberGenerator()

    var body: some Scene {
        WindowGroup {
          ContentView()
            .environmentObject(numberGenerator)
        }
    }
}

Initialize NumberViewModel

When we create NumberViewModel in NumberScreenView, we will need to get the instance of NumberGenerator because our view model currently depends on the NumberGenerator.

Note that we can directly get the NumberGenerator from @EnvironmentObject but we can’t use it in init() constructor. So we need to make the NumberScreenView depend on NumberGenerator.

struct NumberScreenView: View {

  @StateObject private var viewModel: NumberScreenViewModel

  init(numberGenerator: NumberGenerator) {
    _viewModel = StateObject(
      wrappedValue: NumberScreenViewModel(numberGenerator: numberGenerator)
    )
  }

  var body: some View {
    VStack {
      Text(viewModel.number.description)
      Button("Generate New Number") {
        viewModel.getRandomNumber()
      }
    }
    .padding()
  }
}

Initialize NumberScreenView

After that, in the parent view of NumberScreenView(ContentView in this case), we will get the NumberGenerator instance through @EnvironmentObject and use it to initialize NumberScreenView.

struct ContentView: View {
  @EnvironmentObject private var numberGenerator: NumberGenerator

  var body: some View {
    NumberScreenView(numberGenerator: numberGenerator)
  }
}