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.