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?
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.
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:
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:
- You add
ProfileHeaderView
to the top of theVStack
and specify message and video call options are only available if the users are friends. - If the users are friends, you show the friends list, photos and posts.
- 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:
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 withPrivacyLevel
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
.
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!
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.
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:
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:
- The DI Container provides an instance of
PreferencesStore
. - You use the
objectWillChange
property to subscribe to the publisher ofPreferencesStoreProtocol
. - 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())
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:
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.