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!