Dependency Injection Tutorial for iOS: Getting Started

In this tutorial, you’ll learn about Dependency Injection for iOS, as you create the profile page of a social media app in SwiftUI. By Irina Galata.

Leave a rating/review
Download materials
Save for later
Share

Programmers have developed many architectures, design patterns and styles of programming. While they each solve different problems, all of them help make code more readable, testable and flexible.

Inversion of Control is popular for its efficiency. In this tutorial, you’ll apply this principle using the Dependency Injection, or DI, pattern. Instead of using a third party framework you’ll write your own small Dependency Injection solution and use it to refactor an app and add some new features.

If you have no idea what IoC or DI is all about, no problem, you’ll learn more about it soon.

Note: This is an intermediate-level iOS tutorial involving SwiftUI. If you’re unfamiliar with SwiftUI, check out the SwiftUI video course.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of this tutorial. Open the starter project and run the app:

Displaying the user profile in the Sociobox starter project

You’ll see a profile screen from a social media app with a lot of user data: a bio, friends, photos and posts. As with any social network, user privacy and internet safety is essential.

Your goal is to give users control over what information they share with other users. As a bonus, you’ll also give them the ability to adjust the privacy rules depending on their relationship with a given user.

Before you can give them that control and learn more about Dependency Injection and how it can help you, you need to identify the issue.

Identifying the Issue

Open ProfileView.swift and take a closer look at the body of ProfileView:

var body: some View {
  NavigationView {
    ScrollView(.vertical, showsIndicators: true) {
      VStack {
        // 1
        ProfileHeaderView(
          user: user,
          canSendMessage: privacyLevel == .friend,
          canStartVideoChat: privacyLevel == .friend
        )
        // 2
        if privacyLevel == .friend {
          UsersView(title: "Friends", users: user.friends)
          PhotosView(photos: user.photos)
          HistoryFeedView(posts: user.historyFeed)
        } else {
          // 3
          RestrictedAccessView()
        }
      }
    }.navigationTitle("Profile")
  }
}

Here’s a code breakdown:

  1. You add ProfileHeaderView to the top of the VStack and specify message and video call options are only available if the users are friends.
  2. If the users are friends, you show the friends list, photos and posts.
  3. Otherwise, you show the RestrictedAccessView.

The privacyLevel value at the top of ProfileView defines the access level of the user viewing your profile. Change privacyLevel to .everyone and run the app to see your profile as if you were someone outside of your friends list:

User profile view as everyone privacy level

There’s already basic privacy control in place. However, there’s no way for a user to select who sees which sections of their profile. Two privacy levels aren’t sufficient.

Currently, ProfileView decides which views to display depending on the privacy level. This isn’t a proper solution for several reasons:

  • It’s not very testable. While you can cover it with UI tests, they’re more expensive to run than unit or integration tests.
  • Every time you decide to expand or modify your app’s functionality, ProfileView will also require a lot of adaptations. It’s tightly coupled with PrivacyLevel and has more responsibility than needed.
  • As the app’s complexity and functionality grow it’ll get harder to maintain this code.

However, you can improve the situation and seamlessly add new functionality with Dependency Injection.

What Are Inversion of Control and Dependency Injection?

Inversion of Control is a pattern that lets you invert the flow of control. To achieve this you move all the responsibilities of a class, except its main one, outside, making them its dependencies. Through abstraction you make the dependencies easily interchangeable.

Your class, the DI client object, isn’t aware of the implementation its dependencies, the DI service objects. It also doesn’t know how to create them. This makes the code testable and maintainable by eliminating tightly coupled relationships between classes.

Dependency Injection is one of a few patterns that helps apply principles of Inversion of Control. You can implement Dependency Injection in several ways, including Constructor Injection, Setter Injection and Interface Injection.

A common approach is called Constructor Injection. This is the first one you’ll look at.

Constructor Injection

In Constructor Injection, or Initializer Injection, you pass all the class dependencies as constructor parameters. It’s easier to understand what the code does because you immediately see all the dependencies a class needs in one place. For example, look at this snippet:

protocol EngineProtocol {
  func start()
  func stop()
}

protocol TransmissionProtocol {
  func changeGear(gear: Gear)
}

final class Car {
  private let engine: EngineProtocol
  private let transmission: TransmissionProtocol

  init(engine: EngineProtocol, transmission: TransmissionProtocol) {
    self.engine = engine
    self.transmission = transmission
  }
}

In this code snippet, EngineProtocol and TransmissionProtocol are services and Car is the client. Since you split the responsibilities and use abstraction, you can create an instance of Car with any dependencies that conform to the expected protocols. You can even pass test implementations of EngineProtocol and TransmissionProtocol to cover Car with some unit tests.

Next, you’ll look at Setter Injection.

Setter Injection

Setter Injection, or Method Injection, is sightly different. As you can see in this example, it requires dependency setter methods:

final class Car {
  private var engine: EngineProtocol?
  private var transmission: TransmissionProtocol?

  func setEngine(engine: EngineProtocol) {
    self.engine = engine
  }

  func setTransmission(transmission: TransmissionProtocol) {
    self.transmission = transmission
  }
}

This is a good approach when you only have a few dependencies and some are optional. However, it’s easy to forget to set a necessary dependency since nothing forces you to.

Next, you’ll explore Interface Injection.

Interface Injection

Interface Injection requires the client conforms to protocols used to inject dependencies. Look at this example:

protocol EngineMountable {
  func mountEngine(engine: EngineProtocol)
}

protocol TransmissionMountable {
  func mountTransmission(transmission: TransmissionProtocol)
}

final class Car: EngineMountable, TransmissionMountable {
  private var engine: EngineProtocol?
  private var transmission: TransmissionProtocol?

  func mountEngine(engine: EngineProtocol) {
    self.engine = engine
  }

  func mountTransmission(transmission: TransmissionProtocol) {
    self.transmission = transmission
  }
}

Your code gets even more decoupled. In addition, an injector can be completely unaware of the client’s actual implementation.

Dependency Injection Container, or DI Container, is another important Dependency Injection concept. A DI Container is responsible for registering and resolving all dependencies in your project. Depending on the DI Container’s complexity, it can take care of the dependencies’ life cycles and automatically inject them whenever necessary on its own.

In the next section, you’ll create a basic DI Container.