Code has dependencies. It’s one thing that I take into account universally true in a method or one other. Generally these dependencies are third social gathering dependencies whereas different instances you’ll have objects that rely on different objects or performance to perform. Even once you write a perform that needs to 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 wish to deal with explaining dependency injection for Swift. You’ll be taught what dependency injection is, what varieties of dependency injection we’ve got, and also you’ll be taught a bit in regards to the professionals and cons of the totally different approaches.
For those who choose studying via video, have a look right here:
Understanding the fundamentals of dependency injection
Dependency Injection (DI) is a design sample that permits you to decouple parts in your codebase by injecting dependencies from the surface, quite than hardcoding them inside lessons or structs.
For instance, you may need a view mannequin that wants an object to load person information from some information supply. This might be the filesystem, the networking or another place the place information is saved.
Offering this information supply object to your view mannequin is dependency injection. There are a number of methods wherein we are able to inject, and there are other ways to summary these dependencies.
It’s pretty widespread for an object to not rely on a concrete implementation however to rely on 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 sooner or later. And also you could be stunned to seek out out that merely passing an occasion of an object that conforms to DataProviding
is taken into account dependency injection. It’s simply considered one of a number of approaches you possibly can 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 ensure that each object you outline in your code is chargeable for a single factor which signifies that reasoning about elements of your codebase turns into lots less complicated than when you’ve gotten a number of advanced and duplicated logic that’s scattered far and wide.
Let’s take a more in-depth have 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 go 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 social gathering options wanted, and it’s all very specific. For each object you’re in a position to see precisely what that object will rely on.
Extra importantly, it’s additionally a really secure manner of injecting dependencies; you possibly can’t create an occasion of UserViewModel
with out creating and offering your information supplier as nicely.
A draw back of this strategy of dependency injection is that an object may need dependencies that it doesn’t really need. That is very true within the view layer of your app.
Take into account the instance beneath:
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 got an app that has a few views and considered one of our views wants a ProfileDataViewModel
. This view mannequin will be created by the view that sits earlier than it (the MainView
) however that does imply that the MainView
should have the dependencies which might be wanted with a purpose 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 may imply that you just’re passing dependencies throughout a number of layers earlier than they attain the view the place they’re really wanted.
There are a number of approaches to fixing this. We may, for instance, go round an object in our app that is ready to produce view fashions and different dependencies. This object would rely on all of our “core” objects and is able to producing objects that want these “core” objects.
An object that’s in a position to do that is known as a manufacturing facility.
For instance, right here’s what a view mannequin manufacturing facility may 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 may now go our view mannequin manufacturing facility round as a way of fabricating dependencies for our views with out making our views rely on objects they positively don’t want.
We’re nonetheless passing a manufacturing facility round far and wide which you’ll or could not like.
Instead strategy, we are able to work round this with a number of instruments just like the SwiftUI Atmosphere or a software 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 kind of service locator.
So let’s go forward and try 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, it will normally imply that you just first register your dependency within the atmosphere after which take it out in a view. For instance, you possibly can have a look at the code beneath and see precisely how that is achieved.
extension EnvironmentValues {
@Entry var dataProvider = LocalDataProvider()
}
struct MyApp: App {
var physique: some Scene {
WindowGroup {
MainScreen()
.atmosphere(.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 atmosphere in my app struct. Doing this enables me to retrieve this object from the atmosphere wherever I would like it, so I haven’t got to go it from the app struct via probably a number of layers of views. This instance is simplified so the beneifts aren’t large. In an actual app, you’d have extra view layers, and also you’d go dependencies round much more.
With the strategy above, I can put objects within the atmosphere, construct my view hierarchy after which extract no matter I would like on the stage the place I would like it. This enormously simplifies the quantity of code that I’ve to put in writing 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 not likely give me any compile-time security.
What I imply by that’s that if I overlook to register considered one of my dependencies within the atmosphere, I cannot find out about this till I attempt to extract that dependency at runtime. This can be a sample that may exist for any type of service load configuration use, whether or not it is a SwiftUI atmosphere or a third-party library like Resolver.
One other draw back is that my dependencies are actually much more implicit. Which means that although a view will depend on a sure object and I can see that within the record of properties, I can create that object with out placing something in its atmosphere and due to this fact getting crashes when I attempt to seize dependencies from the atmosphere. That is wonderful in smaller apps since you’re extra prone to hit all of the required patterns whereas testing, however in bigger apps, this may be considerably problematic. Once more, we’re missing any type of compile-time security, and that is one thing that I personally miss lots. I like my compiler to assist me write secure code.
That stated, there’s a time and place for service locators, particularly for issues that both have default worth or which might be optionally available or that we inject into the app root and principally our total app will depend on it. So if we’d overlook, we would see crashes as quickly as we launch our app.
The truth that the atmosphere or a dependency locator is much more implicit additionally signifies that we’re by no means fairly certain precisely the place we inject issues within the atmosphere. If the one place we inject from is the summary or the foundation of our utility, it is fairly manageable to see what we do and do not inject. If we additionally make new objects and inject them in the course of our view hierarchy, it turns into lots 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 contemporary one.
That is one thing to bear in mind in the event you select to make heavy use of a service locator just like the SwiftUI atmosphere.
In Abstract
Briefly, dependency injection is a sophisticated time period for a comparatively easy idea.
We need to get dependencies into our objects, and we’d like some mechanism to do that. iOS traditionally would not do a number of third-party frameworks or libraries for dependency injection, so mostly you will both use initializer injection or the SwiftUI atmosphere.
There are third-party libraries that do dependency injection in Swift, however you almost certainly don’t want them.
Whether or not you employ 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 could be a subject that makes use of initializer injection sometimes, and it is only a totally different type of object that you just inject. If you wish to be taught extra about protocol witnesses, I do suggest that you just 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 have any questions or feedback on this put up.