One of the most important topics in software development is how your dependencies are created and how to use them in your code. There are many possibilities out there, but it looks like iOS developers are not used to talk a lot about this. So let’s take a look at some ideas to make dependency injection in your iOS projects.
What is Dependency Injection? Well, let’s say you are writing a class or struct and you need to use some functionality from another one:
class UserListViewModel { var users: [User] = [] func loadUsers() { let service = UserService() service.getUsers { users in self.users = users } } }
Let’s take a look at this code, we are writing a ViewModel that will hold a list of User objects. There is a function loadUsers that will instantiate a service, execute a method and, finally, will save the results in a local property.
That’s great, right? Well, not so much! What if we need to unit test it? We have no way to add a mock in this code, for instance. We are also creating a new instance of the service every time we execute that method. It doesn’t look like a big problem, but imagine thousand of those calls at the same time. We could keep talking about the possible issues, but let’s fix that dependency creation inside the method:
class UserListViewModel { var users: [User] = [] let service = UserService() func loadUsers() { service.getUsers { users in self.users = users } } }
Much better! But we still cannot use a mock for unit testing. In order to inject the service, we could just change it into a var instead of a let . What is the problem of that? Well, we will be creating the default instance even if we will not use it at all. So, another way is to create an initializer:
class UserListViewModel { var users: [User] = [] let service: UserService init(service: UserService) { self.service = service } func loadUsers() { service.getUsers { users in self.users = users } } }
That’s awesome! Well, almost… We have now a great way to inject the dependency, but we don’t have a default value, so it means that we need to always instantiate the service before instantiating the ViewModel . Swift has a couple of ways to add default values, let’s take a look:
class UserListViewModel { var users: [User] = [] let service: UserService init(service: UserService = UserService()) { self.service = service } func loadUsers() { service.getUsers { users in self.users = users } } }
Or
class UserListViewModel { var users: [User] = [] let service: UserService init(service: UserService? = nil) { self.service = service ?? UserService() } func loadUsers() { service.getUsers { users in self.users = users } } }
Well, by just looking at these options, there is really not much difference between them. The second one will need 2 tests to get full coverage, but it is more flexible as you could add some statements to create the dependency with the right options.
One could also say that we could use something like the lazy properties to get dependency injection without the need of initializers:
class UserListViewModel { var users: [User] = [] lazy var service: UserService = UserService() func loadUsers() { service.getUsers { users in self.users = users } } }
That looks great and it really is! You just need to be sure that your dependency is injected before any method try to access it or the default value will be instantiated.
Ok, so we already took a look into a few ways of injecting dependencies in our code, but we cannot leave the fact that we were using a specific object as our dependency. What if the have a totally different implementation of the service and we need to change it in our code? Or just use a mock without subclassing the service? In any of these cases, we would need to manually change the type of our service property. Is there a way to change it in one or just a few places instead of the entire code? Well, actually we do have a great way to do this by simply using protocols!
protocol UserServiceProtocol { func getUsers(completion: @escaping ([User]) -> Void) } class UserListViewModel { var users: [User] = [] let service: UserServiceProtocol init(service: UserServiceProtocol? = nil) { self.service = service ?? UserService() } func loadUsers() { service.getUsers { users in self.users = users } } }
Awesome! Now we can create as many different implementations as we want! The default one would still be the UserService , in many cases it would be fine, but we are looking for a solution that we don’t need even to know which is the default value. In the end, this is what we need, to only know that our dependencies can do some functionalities, not who they are.
So how can we achieve this kind of functionality? Simple, we could use a factory! Sometime ago we found this powerfull framework:
Resolver – Na ultralight dependency Injection / Service Locator framework for swift 5.2 on iOS.
This project is really awesome and it deals with dependency injection in a great way by using a factory and property wrappers! Let’s take a look at how you can create the dependencies:
import Resolver extension Resolver { func setupServices() { register { UserService() as UserServiceProtocol } } }
That code could be executed when your app starts, and this is how you unit will be written:
import Resolver class UserListViewModel { var users: [User] = [] @LazyInjected var service: UserServiceProtocol func loadUsers() { service.getUsers { users in self.users = users } } }
See that property wrapper by the service property? That’s totally amazing! You can choose Resolver – An ultralight Dependency Injection / Service Locator framework for Swift 5.2 on iOS. @Injected to instantiate the dependency at the time of the object creation, @LazyInjected to instantiate the dependency only when the property is accessed and @OptionalInjected to create optional dependencies.
This is just the top of the iceberg for the Resolver framework. We have been using it in some projects here at Poatek and we are loving it!
Do you know any other ways to deal with Dependency Injection in Swift? Please let us now and thanks for reading!
Featured Image from Freepik.