Development
*Before we get started, note that it’s required to have some basic knowledge of closures and generics for this article.
Trying to find an optimal way of injecting services around the codebase isn't a new topic. Developers attempted to do it in numerous ways – either by using singletons around the app or by making a service factory and passing an instance of that factory around. Now, having a lot of singletons isn't a great idea, and the service factory pattern forces you to write the name of services numerous times – something which I, personally, despise. But what if I told you there was a way to write a minimum amount of code that's easy to maintain and test, and isn't hard to improve or adapt?
Say hello to dependency injection with property wrappers!
### What's the deal with dependency injection and services?
Dependency injection is a design pattern achieved by **designing your code in a way that your objects or functions receive objects that they depend on, instead of creating their own**. For example, instead of creating a service around user defaults inside your view models, you should be passed an instance of such service. But where's the benefit in this? Well, testability and ease of refactoring are just some of them. An easy way to achieve this is to pass services you depend on inside to the initializer of your class. That way, if you want to mock a specific service for testing purposes, you can do so without much hassle. Or if you're refactoring a specific service that’s been partially completed, you can supplement a service with another.
```swift
class ViewModel {
private var servicaA: ServiceA
private var serviceB: ServiceB
private var serviceC: ServiceC
init(servicaA: ServiceA, serviceB: ServiceB, serviceC: ServiceC) {
self.servicaA = servicaA
self.serviceB = serviceB
self.serviceC = serviceC
}
}
```
### What are property wrappers?
Property wrappers have been added in Swift 5.1 and as their name suggests, they provide some functionalities around other values. Think of them as property observers (like willSet or didSet) on steroids. They can be used to autocapitalize string properties, or clamp numeric values inside a certain range. You've most definitely worked with them if you ever used SwiftUI. Wrappers like `@State` and `@Binding` are the most common ones. There is also `@AppStorage`, which provides a wrapper for values stored in user defaults. Aside from using them on members of your classes and structs, they can be used on local variables too!
```swift
struct ContentView: View {
@StateObject var viewModel = ViewModel()
@AppStorage("isLoggedIn") private var isLoggedIn: Bool = false
@State private var email = ""
@State private var password = ""
@FocusState var fieldInFocus: Field?
var body: some View {
VStack {
Text("Login")
// other content
}
}
}
```
In this article you will learn how to use property wrappers to achieve dependency injection that looks like this:
```swift
class NewViewModel {
@Service private var servicaA: ServiceA
@Service private var serviceB: ServiceB
@Service private var serviceC: ServiceC
init() {
}
}
```
### The real deal
So here's the idea. We won’t make a service factory, nor store our services inside of a container. We’ll store **the way we create our service** inside a container. Then, using our property wrappers, we'll extract the type of service we need by using type inference, along with generics.
#### Step #1: Service Types
During an app development, you would most often find two types of services inside your app. Ones that should **persist throughout the lifetime of your app – singletons**, and others that should be created on the fly. Think of a bluetooth service that provides a connection to another device – the service should exist only when there's a connection to another device and shouldn’t get recreated while the connection persists. Here are some types of services I’ve encountered so far:
```swift
enum ServiceType {
case singleton
case newSingleton
case new
case automatic
}
```
For ease of use, providing an **automatic** type should handle most of your use cases.
#### Step #2: Service Container
The main idea behind a service container is storing previously created services and the way they are built. That’s why we need a cache of services and a dictionary of closures that generate our services. So inside a class called `ServiceContainer`, place the following:
```swift
private static var factories: [String: () -> Any] = [:]
private static var cache: [String: Any] = [:]
```
The reason why we're using strings as dictionary keys is because services will be recognized by their type names. Now that we have somewhere to cache and store our services, we need a way to register them. Here's how you can do it:
```swift
static func register<Service>(type: Service.Type, _ factory: @autoclosure @escaping () -> Service) {
factories[String(describing: type.self)] = factory
}
```
Notice the `@autoclosure`. The main logic behind registering a service is not registering an instance of the service, but the way it's created. Sometimes you may want to recreate a service that has other dependencies; like time, app state or user state that would set up the service in a different way. This allows shorthand syntax like `ServiceContainer.register(MyServiceProtocol.self, MyService())` that converts `MyService()` to `{ MyService() }` behind the scenes (read: compiler magic). I will get back to why you need to pass the type parameter later on.
Let's get to resolving services. The syntax will start looking a bit crazy right about now, so let's clarify it a bit. Our key is a string describing our type, aka the name of our service. Then, depending on what type of a service we want, we either create one from the dictionary of closures or return a cached one – if we want a singleton. We're also using generics here, hence why we're casting it to our type Service. Generics, in combination with type inference, are a bliss.
```swift
static func resolve<Service>(_ resolveType: ServiceType = .automatic, _ type: Service.Type) -> Service? {
let serviceName = String(describing: type.self)
switch resolveType {
case .singleton:
if let service = cache[serviceName] as? Service {
return service
} else {
let service = factories[serviceName]?() as? Service
if let service = service {
cache[serviceName] = service
}
return service
}
case .newSingleton:
let service = factories[serviceName]?() as? Service
if let service = service {
cache[serviceName] = service
}
return service
case .automatic:
fallthrough
case .new:
return factories[serviceName]?() as? Service
}
}
```
Feel free to add other types of dependencies and modify the resolve function to meet your needs. There's still a catch! **If we don't find a service, we return nil.** But don’t worry, that's something that a property wrapper handles.
#### Step #3: The Property Wrapper
Property Wrappers in Swift are structs annotated with the `@propertyWrapper` annotation and contain a computed property called wrappedValue. This is the value that gets passed to the variable you put the property wrapper on. You can also control what happens if you assign a value to your variable, which is useful when clamping values, for example.
Now that we have a functioning service container, we can extract the service we want from it by using generics. Our whole property wrapper will be using generics and that’s how we’ll know which service to extract from the container. In the initializer of the property wrapper, we try extracting the service type. If it fails, well, it’s the developer’s fault - don’t forget to register that service type!
```swift
@propertyWrapper
struct Service<Service> {
var service: Service
init(_ type: ServiceType = .automatic) {
guard let service = ServiceContainer.resolve(type, Service.self) else {
let serviceName = String(describing: Service.self)
fatalError("No service of type \(serviceName) registered!")
}
self.service = service
}
var wrappedValue: Service {
get { self.service }
mutating set { service = newValue }
}
}
```
You’ll have to be careful if you have a service depending on another service. In other words, if you have ServiceA depending on ServiceB, you’ll have to register ServiceB before registering ServiceA.
Now, notice the mutating setter of the wrappedValue. If you were to only read data from your service, you wouldn’t need to add a setter at all, but there might be some fields you want to modify later on. Think of an image caching service – you need to be able to save an image somewhere.
#### Step #4: @Service in action
Before you start using the `@Service` property wrapper, you need to register all of your dependencies. My suggestion is to set up your ServiceContainer in a separate method in your AppDelegate’s `didFinishLaunchingWithOptions` (or in your App’s initializer if you’re using SwiftUI’s app lifecycle).
```swift
@main
struct ServicesDIApp: App {
init() {
setupServiceContainer()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
private extension ServicesDIApp {
func setupServiceContainer() {
ServiceContainer.register(type: ServiceA.self, ServiceA())
ServiceContainer.register(type: ServiceB.self, ServiceB())
ServiceContainer.register(type: ServiceC.self, ServiceC())
}
}
```
Now, on to the `@Service` part. No matter where you access your dependencies, you can now replace most of your code with the property wrapper. Here are a few examples:
#### 1) Services passed as initializer parameters
If you used proper dependency injection techniques, you are familiar with having numerous parameters for class/struct initializers. The most common case in app development is instantiating view models. Well, when using `@Service`, you can easily delete all service parameters in your initializers and apply `@Service` to the members you were using before.
If you worked on a complex project that dealt in some other way with services and dependency injection, you probably stumbled upon service protocols that your service classes implement. It’s usually a good practice to depend on protocols instead of concrete class implementations since services deal with complex functionalities. If you want to continue that practice, you can pass a protocol type as the first parameter when registering your service. Just make sure to type-annotate your member variables as service protocols, not services themselves. Otherwise you can remove that parameter if you want to.
Using `@Service` isn’t exclusive to view models. You can also use `@Service` inside other services as well. An example of this is having a separate service for networking that handles network requests and using it inside another service that prepares such requests. For instance, if you’ve ever had a service for delivering weather data based on user location, that service would use a device location providing service, along with a network service that would make HTTPs requests to some API.
```swift
class NetworkService {
func fetchData() {
// implementation
}
}
class WeatherDataService {
@Service private var networkService: NetworkService
init() {}
func someMethod() {
networkService.fetchData()
}
}
```
And that’s it, you’re good to go! Notice the amount of code we removed! That’s one of the reasons why I switched to using property wrappers!
#### 2) Services used in method bodies
Sometimes when you’re writing extensions to Swift’s types, you need access to some of your services – for example, when you’re implementing translations inside your app. Let’s say you have a TranslationService that translates any given string to the language you want. To translate a string to another language you could make a computed property that calls the TranslationService and provides you with the appropriate translation. Since property wrappers also work on local variables, you can easily use them inside method bodies, and in this case get access to a TranslationService without a problem.
```swift
extension String {
var translated: String {
@Service var translationService: TranslationService
@Service var userSettingsService: UserSettingsService
let userLanguage = userSettingsService.selectedLanguage
return translationService.translate(key: self, language: userLanguage)
}
}
```
### To summarize
Property wrappers are a really powerful feature in Swift that can be used for dependency injection. They also help you maintain code quality, testability and readability.
If you want to see `@Service` in action, [check out one of my projects that relies heavily on it](https://github.com/bencevicbruno/SwimPal).
I encourage you to experiment with implementing dependency injection using `@Service` into your projects. Maybe you can add more service types or modify the way you resolve or register services. Maybe you can even expand this idea to some other fields inside your project. Let your creativity go wild! 🚀
Bruno is an iOS Developer at COBE. He's passionate about pushing boundaries with what can be achieved using Swift and SwiftUI. As a professional coffee drinker, he spends his energy at the local swimming pool, doing laps.