iOS Architecture - Dependency injection and Inversion of Control [1/3]

Dependency injection, or DI has been a common practice among the backend developers for a long time now, usually we have this topic connected to big projects and solutions, but that is not true of course, mobile applications are not small PoC projects and games anymore as is they used to. Our pocket devices grow on power every year and so the complexity of applications. We need to keep in mind that even small apps with few classes can grow into enterprise size and make order from the beginning, that is why we have SOLID principles and today we are going to dive into the last part, Dependency injection.

Note: If you are not familiar with SOLID principles in general, please refer to a good article here https://itnext.io/solid-principles-explanation-and-examples-715b975dcad4

What will I learn from this article?

I prepared 3 example projects, from very basic solution to demonstrate DI and how it keeps our code clean to reasonably complex IoC container with Property wrappers usage. I will also show you how to write fully testable code for both Unit and UI tests. At the end you will have working component ready to be shipped to your code base.

Note: For more experienced developers already familiar with DI and IoC containers, you can skip to section number 3 for usage with property wrappers and finished solution

1)   Getting started

Let’s see the first example project DIExample01 (https://gitlab.com/ondrej.pisin1/diexample).

You will see:

  • NetworkService and DatabaseService simulating BE and database access
  • DependecyService setting dependencies for this project
  • UsersManager  combining DB and BE requests and providing model for the view
  • UserModel user data representation

Pretty simple right? UsersManager has two dependencies passed as initializer’s parameters, both covered by a protocol, immediately visible for developer and fully testable.

Code

init(networkService: NetworkServiceType, databaseService: DatabaseServiceType) {… }

DependencyService contains all the dependencies, compose them and making them ready for use.

Finally, we can use our manager in UsersViewController and access prepared data:

Code

private let usersManager = DependencyService.shared.usersManager

So far so good.

Note: We have two valid types of passing dependencies, by initializer used here in the first example and by property. It doesn’t really matter which one you use as long as it’s consistent through the project, more common is by property on iOS, because you can’t specify your own initializer in ViewController when using xibs.

Unit tests

I prepared one example unit test for UsersManager to simulate offline condition

Code

testUserManagerReturnsEmptyCollectionWhenOffline()

You can see that I can easily pass my own OfflineNetworkService thanks to open dependencies to the manager and test prepared data for view, in this case empty array. Same goes with every single class, you can prepare any conditions with your own implementations and test the result or specific error.

Conclusion

This way we follow all the SOLID principles and make our code clean, but wait, is that it? Well, not really.

There are some issues:

  • I would like to specify lifecycle of the dependencies, all our services are singletons, but that’s not always the case, we want also lazily initialized singletons, created when we need them and factory types, dependencies created every time when called.
  • It’s too hardcoded, I would like to switch implementations for UI tests, to have the same data on the screen every time I run them

Let’s fix that with an IoC container.

2)   IoC container

Inversion of Control, or IoC is a pattern that lets you move the responsibility for objects creation, lifecycle and injection from all classes to one. Our classes just take given dependencies, work with them and don’t bother with anything else, with abstraction we make the dependencies easily interchangeable. There are many 3th party solutions like Swinject , but with a little effort, we can build our simple IoC container. Let’s open the second example project DIExample02.

DependencyService doesn’t hold hardcoded dependencies anymore, but 3 dictionaries for every lifecycle we want to implement Singeton, LazySingleton, Type and register methods for each of them, register simply puts the provided implementation and type to the right dictionary, where type is the key and object is the value:

Code

var singletons: [ObjectIdentifier: AnyObject] = [:]

func registerSingleton<T, I: AnyObject>(_ interface: T.Type, _ instance: I) {

     let id = ObjectIdentifier(interface)

     self.singletons[id] = instance

 }

We also have resolve, that search the requested type in the collections and returns value, or an error when no implementation found. Resolve can throw an error and is used with try!, don’t get scared, this force unwrap is needed assurance and actually helps a programmer to not forget to register all the implementations. UsersViewController’s dependency changed to this:

Code

let usersManager: UsersManagerType = try! DependencyService.shared.resolve()

One more thing to make the code cleaner:

Code

protocol Injectable {

func register(in container: IoCContainer)

}

func register(_ module: Injectable) {

     module.register(in: self)

}

This protocol helps move all the registrations to a separate class, see the AppModule. Now we can just call DependencyService.shared.register(AppModule()) in the AppDelegate or other place before the actual app launches (If you have some initial loading ViewController for example).

UI tests

Life is so much easier with DI and IoC container, as I wrote earlier you probably need same data every time UI tests are launched for them to be accurate and now we have clean solution how to achieve that.

I added an UI test testUsers() to load UsersViewController, wait for the first cell to appear and then check names. You could notice earlier that there is another registration in AppDelegate:

Code

if CommandLine.arguments.contains("isUITesting") {

     DependencyService.shared.register(UITestModule())

There is another module to mock data for UI tests, so if I set the flag “isUITesting” in UI test:

Code

let app = XCUIApplication()

app.launchArguments = ["isUITesting"]

app.launch()

My mocked implementations are registered as well. Then I test the UI against prepared data set, so again I’m able to test specific scenario like offline mode, empty collection, some notification indicator, etc… 

Note: Remember, it’s UI testing, you should only replace implementation of responsible class right bellow UI, in our case UsersManager, the rest should be covered in unit tests.

3)   Property wrappers

We know how DI works and made IoC container, now thanks to property wrappers added in Swift 5.1, we can implement one more class that saves us a lot of code and make things much easier, sounds good?

Let’s move to the final example project DIExample03, there is one more class called Dependency:

Code

public var wrappedValue: Value {

     do {

            return try DependencyService.shared.resolve()

     } catch {

            let errorDescription = (error as? IoCError)?.localizedDescription ?? "Unknown"

            fatalError("IoC fatal error: \(errorDescription)")

     }

}

Resolve is wrapped in do-catch block to make possible error human readable, so you know what implementation is not added to the container.

“IoC fatal error: Type <UsersManagerType> is not registered.”

 Our dependencies changed to this format:

Code

@Dependency var usersManager: UsersManagerType

We see the tag @Dependency tells right away that class is injected, also registrations are simpler, we don’t need to specify anything in constructor, so from this:

Code

container.registerLazySingleton(UsersManagerType.self, {

return UsersManager(

     networkService: try! container.resolve(),

     databaseService: try! container.resolve())

}) 

to this:

Code

container.registerLazySingleton(UsersManagerType.self, { return UsersManager() })

Looks better right?

What now?

All the materials are here , feel free to use it, I also formed this to a polished library you can see here , it’s also on cocoapods. So that’s it, next part number 2 will be about VIPER architecture, we will see how it works perfectly with DI and IoC container. Also how to add ViewControllers and its lifecycle scoped dependencies to the container too.