Code has dependencies. It’s one thing that I take into account universally true in a technique or one other. Typically these dependencies are third occasion dependencies whereas different occasions you’ll have objects that rely upon different objects or performance to operate. Even whenever you write a operate that must be known as with a easy enter like a quantity, that’s a dependency.
We frequently don’t actually take into account the small issues the be dependencies and this put up is not going to deal with that in any respect. In an earlier put up, I’ve written about utilizing closures as dependencies, often known as protocol witnesses.
On this put up I’d prefer to deal with explaining dependency injection for Swift. You’ll be taught what dependency injection is, what varieties of dependency injection we’ve, and also you’ll be taught a bit concerning the professionals and cons of the totally different approaches.
If you happen to want studying via video, have a look right here:
Understanding the fundamentals of dependency injection
Dependency Injection (DI) is a design sample that lets you decouple parts in your codebase by injecting dependencies from the skin, slightly than hardcoding them inside lessons or structs.
For instance, you might need a view mannequin that wants an object to load consumer knowledge from some knowledge supply. This may very well be the filesystem, the networking or another place the place knowledge is saved.
Offering this knowledge supply object to your view mannequin is dependency injection. There are a number of methods by which we are able to inject, and there are other ways to summary these dependencies.
It’s pretty frequent for an object to not rely upon a concrete implementation however to rely upon a protocol as an alternative:
protocol DataProviding {
func retrieveUserData() async throws -> UserData
}
class LocalDataProvider: DataProviding {
func retrieveUserData() async throws -> UserData {
// learn and return UserData
}
}
class UserProfileViewModel {
let dataProvider: DataProviding
// that is dependency injection
init(dataProvider: DataProviding) {
self.dataProvider = dataProvider
}
}
This code in all probability is one thing you’ve written in some unspecified time in the future. And also you could be stunned to search out out that merely passing an occasion of an object that conforms to DataProviding
is taken into account dependency injection. It’s simply certainly one of a number of approaches you’ll be able to take however in its easiest type, dependency injection is definitely comparatively easy.
Utilizing dependency injection will make your code extra modular, extra reusable, extra testable, and simply overal simpler to work with. You’ll be able to guarantee that each object you outline in your code is answerable for a single factor which implies that reasoning about components of your codebase turns into rather a lot less complicated than when you could have a lot of advanced and duplicated logic that’s scattered in every single place.
Let’s take a better take a look at initializer injection which is the type of dependency injection that’s used within the code above.
Initializer injection
Initializer injection is a type of dependency injection the place you explicitly cross an object’s dependencies to its initializer. Within the instance you noticed earlier, I used initializer injection to permit my UserProfileViewModel
to obtain an occasion of an object that conforms to DataProviding
as a dependency.
Passing dependencies round like that is probably the best type of passing dependencies round. It doesn’t require any setup, there’s no third occasion options wanted, and it’s all very specific. For each object you’re capable of see precisely what that object will rely upon.
Extra importantly, it’s additionally a really protected manner of injecting dependencies; you’ll be able to’t create an occasion of UserViewModel
with out creating and offering your knowledge supplier as effectively.
A draw back of this strategy of dependency injection is that an object might need dependencies that it doesn’t really need. That is very true within the view layer of your app.
Take into account the instance under:
struct MyApp: App {
let dataProvider = LocalDataProvider()
var physique: some Scene {
WindowGroup {
MainScreen()
}
}
}
struct MainScreen: View {
let dataProvider: DataProviding
var physique: some View {
NavigationStack {
// ... some views
UserProfileView(viewModel: UserProfileViewModel(dataProvider: dataProvider))
}
}
}
On this instance, we’ve an app that has a few views and certainly one of our views wants a ProfileDataViewModel
. This view mannequin might be created by the view that sits earlier than it (the MainView
) however that does imply that the MainView
should have the dependencies which can be wanted with a view to create the ProfileDataViewModel
. The result’s that we’re creating views which have dependencies that they don’t technically want however we’re required to supply them as a result of some view deeper within the view hierarchy does want that dependency.
In bigger apps this would possibly imply that you simply’re passing dependencies throughout a number of layers earlier than they attain the view the place they’re truly wanted.
There are a number of approaches to fixing this. We might, for instance, cross round an object in our app that is ready to produce view fashions and different dependencies. This object would rely upon all of our “core” objects and is able to producing objects that want these “core” objects.
An object that’s ready to do that is known as a manufacturing facility.
For instance, right here’s what a view mannequin manufacturing facility might appear like:
struct ViewModelFactory {
non-public let dataProvider: DataProviding
func makeUserProfileViewModel() -> UserProfileViewModel {
return UserProfileViewModel(dataProvider: dataProvider)
}
// ...
}
As a substitute of passing particular person dependencies round all through our app, we might now cross our view mannequin manufacturing facility round as a way of fabricating dependencies for our views with out making our views rely upon objects they undoubtedly don’t want.
We’re nonetheless passing a manufacturing facility round in every single place which you will or could not like.
In its place strategy, we are able to work round this with a number of instruments just like the SwiftUI Atmosphere or a instrument like Resolver. Whereas these two instruments are very totally different (and the main points are out of scope for this put up), they’re each a sort of service locator.
So let’s go forward and check out how service locators are used subsequent.
Service locators
The service locator sample is a design sample that can be utilized for dependency injection. The best way a service locator works is that just about like a dictionary that incorporates all of our dependencies.
Working with a service locator sometimes is a two-step course of:
- Register your dependency on the locator
- Extract your dependency from the locator
In SwiftUI, this may normally imply that you simply first register your dependency within the surroundings after which take it out in a view. For instance, you’ll be able to take a look at the code under and see precisely how that is completed.
extension EnvironmentValues {
@Entry var dataProvider = LocalDataProvider()
}
struct MyApp: App {
var physique: some Scene {
WindowGroup {
MainScreen()
.surroundings(.dataProvider, LocalDataProvider())
}
}
}
struct MainScreen: View {
@Atmosphere(.dataProvider) var dataProvider
var physique: some View {
NavigationStack {
// ... some views
UserProfileView(viewModel: UserProfileViewModel(dataProvider: dataProvider))
}
}
}
On this code pattern, I register my view mannequin and an information supplier object on the surroundings in my app struct. Doing this permits me to retrieve this object from the surroundings wherever I need it, so I haven’t got to cross it from the app struct via probably a number of layers of views. This instance is simplified so the beneifts aren’t enormous. In an actual app, you’d have extra view layers, and also you’d cross dependencies round much more.
With the strategy above, I can put objects within the surroundings, construct my view hierarchy after which extract no matter I want on the degree the place I want it. This drastically simplifies the quantity of code that I’ve to jot down to get a dependency to the place it must be and I will not have any views which have dependencies that they do not technically want (like I do with initializer injection).
The draw back is that this strategy does probably not give me any compile-time security.
What I imply by that’s that if I neglect to register certainly one of my dependencies within the surroundings, I can’t learn about this till I attempt to extract that dependency at runtime. This can be a sample that may exist for any form of service load configuration use, whether or not it is a SwiftUI surroundings or a third-party library like Resolver.
One other draw back is that my dependencies are actually much more implicit. Because of this despite the fact that a view is determined by a sure object and I can see that within the listing of properties, I can create that object with out placing something in its surroundings and subsequently getting crashes when I attempt to seize dependencies from the surroundings. That is superb in smaller apps since you’re extra more likely to hit all of the required patterns whereas testing, however in bigger apps, this may be considerably problematic. Once more, we’re missing any form of compile-time security, and that is one thing that I personally miss rather a lot. I like my compiler to assist me write protected code.
That stated, there’s a time and place for service locators, particularly for issues that both have an excellent default worth or which can be non-compulsory or that we inject into the app root and mainly our complete app is determined by it. So if we’d neglect, we would see crashes as quickly as we launch our app.
The truth that the surroundings or a dependency locator is much more implicit additionally implies that we’re by no means fairly certain precisely the place we inject issues within the surroundings. If the one place we inject from is the summary or the foundation of our software, it is fairly manageable to see what we do and do not inject. If we additionally make new objects and inject them in the midst of our view hierarchy, it turns into rather a lot trickier to purpose about precisely the place a dependency is created and injected. And extra importantly, it additionally would not actually make it apparent if at any level we overwrite a dependency or if we’re injecting a recent one.
That is one thing to bear in mind if you happen to select to make heavy use of a service locator just like the SwiftUI surroundings.
In Abstract
In brief, dependency injection is an advanced time period for a comparatively easy idea.
We wish to get dependencies into our objects, and we want some mechanism to do that. iOS traditionally would not do plenty of third-party frameworks or libraries for dependency injection, so mostly you will both use initializer injection or the SwiftUI surroundings.
There are third-party libraries that do dependency injection in Swift, however you probably don’t want them.
Whether or not you utilize initializer injection or the service locator sample, it is considerably of a mixture between a desire and a trade-off between compile-time security and comfort.
I did not cowl issues like protocol witnesses on this put up as a result of that may be a matter that makes use of initializer injection sometimes, and it is only a totally different form of object that you simply inject. If you wish to be taught extra about protocol witnesses, I do advocate that you simply check out my weblog put up the place I discuss utilizing closures as dependencies.
I hope you loved this put up. I hope it taught you a large number about dependency injection. And don’t hesitate to achieve out to me when you’ve got any questions or feedback on this put up.