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.
        
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Dependency Injection Tutorial for iOS: Getting Started
25 mins
- Getting Started
- Identifying the Issue
- What Are Inversion of Control and Dependency Injection?
- Constructor Injection
- Setter Injection
- Interface Injection
- Using Dependency Injection
- Using a Dependency Injection Container
- Extending the Functionality
- Adding User Preferences
- Adding the Preferences Screen
- Adding Combine
- Bringing It All Together
- Where to Go From Here?
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.
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:
- First, you make a static property of type DIContainer.
- 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.
- Then you create a dictionary to keep all the services.
- The string representation of the type of the component is the key in the dictionary.
- You can use the type again to resolve the necessary dependency.
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. :]
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:
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!

