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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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!