Code has dependencies. It’s one thing that I contemplate universally true in a technique or one other. Typically these dependencies are third celebration dependencies whereas different occasions you’ll have objects that rely on different objects or performance to operate. Even whenever you write a operate that ought to be known as with a easy enter like a quantity, that’s a dependency.
We frequently don’t actually contemplate the small issues the be dependencies and this put up won’t give attention to that in any respect. In an earlier put up, I’ve written about utilizing closures as dependencies, also referred to as protocol witnesses.
On this put up I’d prefer to give attention to explaining dependency injection for Swift. You’ll study what dependency injection is, what sorts of dependency injection we have now, and also you’ll study a bit concerning the professionals and cons of the totally different approaches.
For those who desire studying by means of video, have a look right here:
Understanding the fundamentals of dependency injection
Dependency Injection (DI) is a design sample that means that you can decouple parts in your codebase by injecting dependencies from the surface, reasonably than hardcoding them inside courses or structs.
For instance, you may need a view mannequin that wants an object to load consumer knowledge from some knowledge supply. This might 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 during which we will 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 a substitute:
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 search 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 may 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 possibly can make it possible for each object you outline in your code is answerable for a single factor which implies that reasoning about elements of your codebase turns into lots less complicated than when you’ve a number 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 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 almost definitely the only type of passing dependencies round. It doesn’t require any setup, there’s no third celebration 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 protected approach of injecting dependencies; you may’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 may need dependencies that it doesn’t really need. That is very true within the view layer of your app.
Contemplate 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 have now 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
will need to have the dependencies which are wanted in an effort 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 offer 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 just’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 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 to be:
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 method of fabricating dependencies for our views with out making our views rely on 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 will work round this with a number of instruments just like the SwiftUI Surroundings or a device 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 nearly like a dictionary that comprises 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 just first register your dependency within the atmosphere after which take it out in a view. For instance, you may take a look at the code beneath and see precisely how that is finished.
extension EnvironmentValues {
@Entry var dataProvider = LocalDataProvider()
}
struct MyApp: App {
var physique: some Scene {
WindowGroup {
MainScreen()
.atmosphere(.dataProvider, LocalDataProvider())
}
}
}
struct MainScreen: View {
@Surroundings(.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 a knowledge supplier object on the atmosphere in my app struct. Doing this permits me to retrieve this object from the atmosphere wherever I need it, so I haven’t got to go it from the app struct by means of probably a number of layers of views. This instance is simplified so the beneifts aren’t big. 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 vastly simplifies the quantity of code that I’ve to write 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 considered one of my dependencies within the atmosphere, I cannot learn about this till I attempt to extract that dependency at runtime. This can be a sample that can exist for any form 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 though a view will depend on a sure object and I can see that within the checklist 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 advantageous 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 lots. 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 a very good default worth or which are non-compulsory or that we inject into the app root and mainly our complete app will depend on it. So if we might neglect, 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 implies 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 midst of our view hierarchy, it turns into lots trickier to motive about precisely the place a dependency is created and injected. And extra importantly, it additionally does 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 case you select to make heavy use of a service locator just like the SwiftUI atmosphere.
In Abstract
Briefly, dependency injection is an advanced 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 does not do a number of third-party frameworks or libraries for dependency injection, so mostly you may both use initializer injection or the SwiftUI atmosphere.
There are third-party libraries that do dependency injection in Swift, however you probably 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 may be a subject that makes use of initializer injection sometimes, and it is only a totally different form of object that you just inject. If you wish to study extra about protocol witnesses, I do advocate 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 numerous about dependency injection. And don’t hesitate to achieve out to me you probably have any questions or feedback on this put up.