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?
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: