Advanced Git, Second Edition

Git is key to great version control and collaboration on software projects.
Stop struggling with Git and spend more time on the stuff that matters!

Home iOS & Swift Tutorials

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.

4.3/5 20 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

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.

Using Dependency Injection

Finally, it’s time to apply your knowledge of the pattern! Create a new Swift file named ProfileContentProvider with the following:

import SwiftUI

protocol ProfileContentProviderProtocol {
  var privacyLevel: PrivacyLevel { get }
  var canSendMessage: Bool { get }
  var canStartVideoChat: Bool { get }
  var photosView: AnyView { get }
  var feedView: AnyView { get }
  var friendsView: AnyView { get }
}

While this code is only a protocol, the implementation decides what kind of content to provide.

Next, add the following class below the protocol you added:

final class ProfileContentProvider: ProfileContentProviderProtocol {
  let privacyLevel: PrivacyLevel
  private let user: User

  init(privacyLevel: PrivacyLevel, user: User) {
    self.privacyLevel = privacyLevel
    self.user = user
  }

  var canSendMessage: Bool {
    privacyLevel > .everyone
  }

  var canStartVideoChat: Bool {
    privacyLevel > .everyone
  }

  var photosView: AnyView {
    privacyLevel > .everyone ? 
      AnyView(PhotosView(photos: user.photos)) : 
      AnyView(EmptyView())
  }

  var feedView: AnyView {
    privacyLevel > .everyone ? 
      AnyView(HistoryFeedView(posts: user.historyFeed)) : 
      AnyView(RestrictedAccessView())
  }

  var friendsView: AnyView {
    privacyLevel > .everyone ? 
      AnyView(UsersView(title: "Friends", users: user.friends)) : 
      AnyView(EmptyView())
  }
}

Now you have a separate provider with one responsibility: Decide how to display the user profile depending on the privacy level.

Next, switch to ProfileView.swift and add the following code right above ProfileView‘s body property:

private let provider: ProfileContentProviderProtocol

init(provider: ProfileContentProviderProtocol, user: User) {
  self.provider = provider
  self.user = user
}

You set ProfileView‘s user variable in its initialize, so remove the Mock.user() value assignment.

Now, update ProfileView‘s body property as follows:

var body: some View {
  NavigationView {
    ScrollView(.vertical, showsIndicators: true) {
      VStack {
        ProfileHeaderView(
          user: user,
          canSendMessage: provider.canSendMessage,
          canStartVideoChat: provider.canStartVideoChat
        )
        provider.friendsView
        provider.photosView
        provider.feedView
      }
    }.navigationTitle("Profile")
  }
}

With these changes ProfileView no longer depends on the privacyLevel variable because it receives necessary dependencies via its initializer, Constructor Injection. Remove the privacyLevel constant from ProfileView.

Note: You’ll see Xcode complaining that it’s missing arguments in ProfileView_Previews. Don’t worry; you’ll fix this shortly.

This is where you start seeing the beauty of the approach. The view is now completely unaware of the business logic behind the profile contents. You can give any implementation of ProfileContentProviderProtocol, include new privacy levels or even mock the provider without changing a single line of code!

You’ll verify this in a few moments. First, it’s time to set up your Dependency Injection Container to help collect all of your DI infrastructure in one place.

Using a Dependency Injection Container

Now, create a new file named DIContainer.swift and add the following:

protocol DIContainerProtocol {
  func register<Component>(type: Component.Type, component: Any)
  func resolve<Component>(type: Component.Type) -> Component?
}

final class DIContainer: DIContainerProtocol {
  // 1
  static let shared = DIContainer()
  
  // 2
  private init() {}

  // 3
  var components: [String: Any] = [:]

  func register<Component>(type: Component.Type, component: Any) {
    // 4
    components["\(type)"] = component
  }

  func resolve<Component>(type: Component.Type) -> Component? {
    // 5
    return components["\(type)"] as? Component
  }
}

Here’s a step-by-step explanation:

  1. First, you make a static property of type DIContainer.
  2. Since you mark the initializer as private, you essentially ensure your container is a singleton. This prevents any unintentional use of multiple instances and unexpected behavior, like missing some dependencies.
  3. Then you create a dictionary to keep all the services.
  4. The string representation of the type of the component is the key in the dictionary.
  5. You can use the type again to resolve the necessary dependency.
Note: Essentially, the DI Container, like any other pattern, is an approach for solving a programming problem. You can implement it several ways including third party frameworks.

Next, to make your container handle the dependencies, open ProfileView.swift and update the initializer of ProfileView as follows:

init(
  provider: ProfileContentProviderProtocol = 
    DIContainer.shared.resolve(type: ProfileContentProviderProtocol.self)!,
  user: User = DIContainer.shared.resolve(type: User.self)!
) {
  self.provider = provider
  self.user = user
}

Now your DIContainer provides the necessary parameters by default. However, you can always pass in dependencies on your own for testing purposes or to register mocked dependencies in the container.

Next, find ProfileView_Previews below ProfileView and update it:

struct ProfileView_Previews: PreviewProvider {
  private static let user = Mock.user()
  static var previews: some View {
    ProfileView(
      provider: ProfileContentProvider(privacyLevel: .friend, user: user), 
      user: user)
  }
}

Open ProfileContentProvider.swift. Update the initializer of ProfileContentProvider to use the same approach:

init(
  privacyLevel: PrivacyLevel = 
    DIContainer.shared.resolve(type: PrivacyLevel.self)!,
  user: User = DIContainer.shared.resolve(type: User.self)!
) {
  self.privacyLevel = privacyLevel
  self.user = user
}

Finally, you must define the initial state of your dependencies to replicate the behavior of the app before you began working on it.

In SceneDelegate.swift add the following code above the initialization of profileView:

let container = DIContainer.shared
container.register(type: PrivacyLevel.self, component: PrivacyLevel.friend)
container.register(type: User.self, component: Mock.user())
container.register(
  type: ProfileContentProviderProtocol.self, 
  component: ProfileContentProvider())

Build and run. While the app looks exactly as it did before, you know how much more beautiful it is inside. :]

After refactor UI still looks the same

Next, you’ll implement new functionality.

Extending the Functionality

Sometimes a user wants to hide some content or functionality from people in their friends list. Maybe they post pictures from parties which they want only close friends to see. Or perhaps they only want to receive video calls from close friends.

Regardless of the reason, the ability to give close friends extra access rights is a great feature.

To implement it, go to PrivacyLevel.swift and add another case:

enum PrivacyLevel: Comparable {
  case everyone, friend, closeFriend
}

Next, update the provider which will handle a new privacy level. Go to ProfileContentProvider.swift and update the following properties:

var canStartVideoChat: Bool {
  privacyLevel > .friend
}

var photosView: AnyView {
  privacyLevel > .friend ? 
    AnyView(PhotosView(photos: user.photos)) : 
    AnyView(EmptyView())
}

With this code you ensure only close friends can access photos and initiate a video call. You don’t need to make any other changes to add additional privacy levels. You can create as many privacy levels or groups as you need, give a provider to ProfileView and everything else is handled for you.

Now, build and run:

The video call button and recent photos section are restricted to close friends

As you can see, the video call icon and the recent photos section are now gone for the .friend privacy level. You achieved your goal!

Adding User Preferences

Want to try a more complicated use case? What if you need to make your provider base its decisions on the user’s privacy preferences?

To solve this problem, you’ll add a new screen where users can decide who can access each part of their profile, save preferences using UserDefaults and reload the profile screen whenever they update a preference. You’ll use the Combine framework to make it work.

Note: If you want to become more familiar with Combine or refresh your knowledge, take a look at this Combine: Getting Started tutorial.

First, open PrivacyLevel.swift and add the following property and method to PrivacyLevel:

var title: String {
  switch self {
  case .everyone:
    return "Everyone"
  case .friend:
    return "Friends only"
  case .closeFriend:
    return "Close friends only"
  }
}

static func from(string: String) -> PrivacyLevel? {
  switch string {
  case everyone.title:
    return everyone
  case friend.title:
    return friend
  case closeFriend.title:
    return closeFriend
  default:
    return nil
  }
}

You’ll use title to display the privacy level options on a new preferences screen you’re about to create. from(string:) helps recreate a PrivacyLevel from a saved UserDefaults preference.

Now right-click the Sociobox folder in the Project navigator and select Add Files to “Sociobox”…. Choose PreferencesStore.swift and click Add. Open the file and look through the code.

It’s a class responsible for saving and reading user preferences from UserDefaults.

You have a property for each of the five profile sections and a method to reset the preferences. PreferencesStoreProtocol conforms to the ObservableObject protocol, making your store have a publisher that will emit whenever any of the properties marked with the @Published attribute change.

When there are any changes, any SwiftUI view, or even a regular class, can subscribe to PreferencesStoreProtocol and reload its content.

Next, you’ll add the Preferences Screen.

Adding the Preferences Screen

Now, right-click the Views folder and, once again, select Add Files to “Sociobox”… to add UserPreferencesView.swift. Open it and take a look at the preview:

Preview of preferences screen for privacy settings

This is what your new screen will look like.

Make the new screen save user preferences by implementing the PreferencesStoreProtocol. Update the declaration of UserPreferencesView to the following:

struct UserPreferencesView<Store>: View where Store: PreferencesStoreProtocol {

Like in every statically typed programming language, types are defined and checked at compile time. And here’s the problem: You don’t know the exact type Store will have at runtime, but don’t panic! What you do know is that Store will conform to PreferencesStoreProtocol. So, you tell the compiler that Store will implement this protocol.

The compiler needs to know which specific type you want to use for your view. Later on, when you create an instance of UserPreferencesView, you’ll need to use a specific type instead of a protocol in the angle brackets, like this:

UserPreferencesView<PreferencesStore>()

This way, the type can be checked at compile time. Now, add the following property and initializer to UserPreferencesView:

private var store: Store

init(store: Store = DIContainer.shared.resolve(type: Store.self)!) {
  self.store = store
}

With the code above, you let your UserPreferencesView receive the needed dependency, instead of creating it on its own.

Update the body property to use the store to access user preferences:

var body: some View {
  NavigationView {
    VStack {
      PreferenceView(title: .photos, value: store.photosPreference) { value in
        store.photosPreference = value
      }
      PreferenceView(
        title: .friends, 
        value: store.friendsListPreference
      ) { value in
        store.friendsListPreference = value
      }
      PreferenceView(title: .feed, value: store.feedPreference) { value in
        store.feedPreference = value
      }
      PreferenceView(
        title: .videoCall, 
        value: store.videoCallsPreference
      ) { value in
        store.videoCallsPreference = value
      }
      PreferenceView(
        title: .message, 
        value: store.messagePreference
      ) { value in
        store.messagePreference = value
      }
      Spacer()
    }
  }.navigationBarTitle("Privacy preferences")
}

Here’s a code breakdown:

  • Each PreferenceView in the vertical stack represents a different profile section with a drop down menu to select a privacy level.
  • Read the current value of each preference from the store.
  • When the user chooses a privacy option, save the new value to the store.

Update the previews property of UserPreferencesView_Previews, so you can see the preview again:

static var previews: some View {
  UserPreferencesView(store: PreferencesStore())
}

In SceneDelegate.swift, register the store dependency in your container:

container.register(type: PreferencesStore.self, component: PreferencesStore())

Adding Combine

Next, go to ProfileContentProvider.swift and import Combine at the top of the file:

import Combine

Then, update its declaration as you did with UserPreferencesView:

final class ProfileContentProvider<Store>: ProfileContentProviderProtocol 
  where Store: PreferencesStoreProtocol {

Now, update the declaration of ProfileContentProviderProtocol:

protocol ProfileContentProviderProtocol: ObservableObject {

This code lets ProfileView subscribe to changes in ProfileContentProvider and update the state immediately when a user selects a new preference.

In ProfileContentProvider, add a property for the store and replace the initializer:

private var store: Store
private var cancellables: Set<AnyCancellable> = []

init(
  privacyLevel: PrivacyLevel = 
    DIContainer.shared.resolve(type: PrivacyLevel.self)!,
  user: User = DIContainer.shared.resolve(type: User.self)!,
  // 1
  store: Store = DIContainer.shared.resolve(type: Store.self)!
) {
  self.privacyLevel = privacyLevel
  self.user = user
  self.store = store

  // 2
  store.objectWillChange.sink { _ in
    self.objectWillChange.send()
  }
  .store(in: &cancellables)
}

Here’s what you did:

  1. The DI Container provides an instance of PreferencesStore.
  2. You use the objectWillChange property to subscribe to the publisher of PreferencesStoreProtocol.
  3. You make the publisher of ProfileContentProviderProtocol emit as well when a property changes in the store.

Now, update ProfileContentProvider‘s properties to use the properties of the store instead of instances of the PrivacyLevel enum:

var canSendMessage: Bool {
  privacyLevel >= store.messagePreference
}

var canStartVideoChat: Bool {
  privacyLevel >= store.videoCallsPreference
}

var photosView: AnyView {
  privacyLevel >= store.photosPreference ? 
    AnyView(PhotosView(photos: user.photos)) : 
    AnyView(EmptyView())
}

var feedView: AnyView {
  privacyLevel >= store.feedPreference ? 
    AnyView(HistoryFeedView(posts: user.historyFeed)) : 
    AnyView(EmptyView())
}

var friendsView: AnyView {
  privacyLevel >= store.friendsListPreference ? 
    AnyView(UsersView(title: "Friends", users: user.friends)) : 
    AnyView(EmptyView())
}

Everything stayed the same except you no longer use the enum directly.

Bringing It All Together

To subscribe to the changes in the provider, open ProfileView.swift and change the declaration of ProfileView as well:

struct ProfileView<ContentProvider>: View 
  where ContentProvider: ProfileContentProviderProtocol {

Update the provider property to use the generic:

@ObservedObject private var provider: ContentProvider

When you use @ObservedObject in your SwiftUI view, you subscribe to its publisher. The view reloads itself when it emits.

Update the initializer as well:

init(
  provider: ContentProvider = 
    DIContainer.shared.resolve(type: ContentProvider.self)!,
  user: User = DIContainer.shared.resolve(type: User.self)!
) {
  self.provider = provider
  self.user = user
}

Then add this code right below navigationTitle("Profile") inside body property:

.navigationBarItems(trailing: Button(action: {}) {
  NavigationLink(destination: UserPreferencesView<PreferencesStore>()) {
    Image(systemName: "gear")
  }
})

You added a navigation bar button which will take users to the preferences screen.

Now go back to SceneDelegate.swift to update the dependencies registration. As quite a few of your protocols and classes are generic, using them all together is becoming a bit hard to read.

To make it easier, create a new typealias above scene(_:willConnectTo:options:) for the provider:

typealias Provider = ProfileContentProvider<PreferencesStore>

Use the new typealias by removing:

container.register(
  type: ProfileContentProviderProtocol.self, 
  component: ProfileContentProvider())

Now, add the following _after_ the call to register PreferencesStore:

container.register(type: Provider.self, component: Provider())
Note: You must register Provider last because its initializer expects privacy level, user and store to exist already in the DI Container.

Add <Provider> to the initialization of profileView:

let profileView = ProfileView<Provider>()

For a usable preview, open ProfileView.swift and add the same setup in ProfileView_Previews:

struct ProfileView_Previews: PreviewProvider {
  static var previews: some View {
    typealias Provider = ProfileContentProvider<PreferencesStore>
    let container = DIContainer.shared
    container.register(type: PrivacyLevel.self, component: PrivacyLevel.friend)
    container.register(type: User.self, component: Mock.user())
    container.register(
      type: PreferencesStore.self, 
      component: PreferencesStore())
    container.register(type: Provider.self, component: Provider())
    return ProfileView<Provider>()
  }
}

After your hard work, it’s time to see how it works all together. Run the app to see the result:

Demonstrating the preferences screen in action

Where to Go From Here?

You can download the completed project by clicking the Download Materials button at the top or bottom of this tutorial.

In this tutorial, you learned about the Dependency Injection pattern and how to build and apply it in your project. Depending on the project you’re working on, you may consider using a third party solution.

To learn more, read our tutorial on Swinject. You’ll find some testing examples which are handy even if you’re not using a third party framework for dependency injection.

If you have any questions or comments, don’t hesitate to reach out in the forum discussion below.

Average Rating

4.3/5

Add a rating for this content

20 ratings

More like this

Contributors

Comments