7.7 C
New York
Wednesday, October 16, 2024

What’s dependency injection in Swift? – Donny Wals


Code has dependencies. It’s one thing that I take into account universally true in a method or one other. Typically these dependencies are third occasion dependencies whereas different instances you’ll have objects that rely on different objects or performance to operate. Even if you write a operate that needs to be referred to 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 submit is not going to concentrate on that in any respect. In an earlier submit, I’ve written about utilizing closures as dependencies, also referred to as protocol witnesses.

On this submit I’d prefer to concentrate on explaining dependency injection for Swift. You’ll study what dependency injection is, what forms of dependency injection we’ve, and also you’ll study a bit concerning the execs and cons of the totally different approaches.

For those who choose studying by way of 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 skin, slightly than hardcoding them inside courses or structs.

For instance, you may need a view mannequin that wants an object to load person 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 are able to inject, and there are alternative ways to summary these dependencies.

It’s pretty frequent 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 in some unspecified time in the future. And also you is likely to 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 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 possibly can guarantee that each object you outline in your code is answerable for a single factor which implies that reasoning about elements of your codebase turns into quite a bit less complicated than when you will have numerous complicated and duplicated logic that’s scattered everywhere.

Let’s take a better 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 move 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 more than likely 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 express. For each object you’re capable of see precisely what that object will rely on.

Extra importantly, it’s additionally a really secure approach of injecting dependencies; you’ll be able to’t create an occasion of UserViewModel with out creating and offering your knowledge supplier as nicely.

A draw back of this method 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’ve an app that has a few views and certainly 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 might be 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 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 truly wanted.

There are a number of approaches to fixing this. We may, for instance, move 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 unit.

For instance, right here’s what a view mannequin manufacturing unit may appear like:

struct ViewModelFactory {
  non-public let dataProvider: DataProviding

  func makeUserProfileViewModel() -> UserProfileViewModel {
    return UserProfileViewModel(dataProvider: dataProvider)
  }

  // ...
}

As an alternative of passing particular person dependencies round all through our app, we may now move our view mannequin manufacturing unit round as a method 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 unit round everywhere which you will or might not like.

As a substitute method, we are able to 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 small print are out of scope for this submit), they’re each a sort 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 way in which a service locator works is that nearly like a dictionary that comprises all of our dependencies.

Working with a service locator usually is a two-step course of:

  1. Register your dependency on the locator
  2. Extract your dependency from the locator

In SwiftUI, this can normally imply that you just first register your dependency within the surroundings after which take it out in a view. For instance, you’ll be able to 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()
      .surroundings(.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 surroundings in my app struct. Doing this permits me to retrieve this object from the surroundings wherever I would like it, so I haven’t got to move it from the app struct by way of doubtlessly 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 move dependencies round much more.

With the method above, I can put objects within the surroundings, 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 method does not likely 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 cannot find out about this till I attempt to extract that dependency at runtime. It is a sample that can exist for any type 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 at the moment are much more implicit. Which means although a view is dependent upon a sure object and I can see that within the checklist 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 fantastic 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 quite a bit. I like my compiler to assist me write secure code.

That mentioned, there’s a time and place for service locators, particularly for issues that both have a very good default worth or which might be non-obligatory or that we inject into the app root and mainly our whole app is dependent upon 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 course of our view hierarchy, it turns into quite a bit 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 surroundings.

In Abstract

Briefly, 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 does not do a whole lot of third-party frameworks or libraries for dependency injection, so mostly you may 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 employ initializer injection or the service locator sample, it is considerably of a mixture between a choice and a trade-off between compile-time security and comfort.

I did not cowl issues like protocol witnesses on this submit as a result of that may be a matter that makes use of initializer injection usually, and it is only a totally different type 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 submit the place I speak about utilizing closures as dependencies.

I hope you loved this submit. I hope it taught you numerous about dependency injection. And don’t hesitate to succeed in out to me when you’ve got any questions or feedback on this submit.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles