In an object-oriented application, objects will work together to provide the application’s functionality. A popular way to allow these objects to work together is known as dependency injection.
You can read a good primer on what dependency injection (or DI) is all about here – it essentially means that objects aren’t responsible for locating the other objects they need in order to perform their function. It has many benefits: it makes the code simpler and easier to reason about; it makes objects easier to test; and it means that objects can have a single responsibility, which is a good design practice.
For these reasons we’ve always used DI when assembling objects in the Bipsync Notes app (and indeed in all our apps). Our approach was straightforward – the current coordinator that was controlling the app would assemble any services that were needed by that section of the app and inject them into the root view controller, where they’d then be passed along to subsequent view controllers as segues were performed. While it worked, this approach was not without its problems:
- We have a lot of objects to assemble, so the coordinators were becoming bloated with code related to instantiation and configuration of objects. Their job is to orchestrate, so this was violating the single responsibility principle while making the classes unwieldy.
- Often we’d be passing a service through four or five view controllers just to get it to the object that required it. It was long winded to implement, made refactoring more difficult and meant view controllers had to worry about services they didn’t necessarily care about.
To address these issues we decided that we needed to:
- Extract code relating to object instantiation/configuration from the coordinators to somewhere more suitable.
- Use a container to house and retain service objects.
- Find a way to inject dependencies into a view controller at any point in the segue chain, without having to pass them up through the hierarchy.
Attempt #1: Typhoon
Googling for ‘objective-c dependency injection’ revealed a promising option: Typhoon. Further investigation revealed it to be the most starred DI project on Github and the most popular DI library on Cocoapods so it seemed a natural place to start.
Typhoon is a complex library, which isn’t helped by its somewhat muddled documentation, which I personally found difficult to get to grips with – I must’ve read some pages ten, twenty times before I understood what I needed to do. With Typhoon you create a number of assemblies which go through two lifecycle phases. In phase one an assembly dictates the wiring between objects that defines their dependencies. In phase two, after “activation”, those dependencies are resolved so the assemblies now contain the fully initialised objects. Once I had that understanding and, crucially, could see how it fitted into our app’s startup flow, I began to integrate.
I soon hit a problem I couldn’t resolve. One of the features of Typhoon allows it to inject dependencies into view controllers within storyboards, but this activation was happening immediately after the app was launched. At this point we’re not ready to begin dependency resolution because a number of our services (database, note editor) are created asynchronously. This didn’t jive with the way Typhoon works – I believe so it can support state restoration, which we don’t currently do – and I couldn’t find a way to get around it.
So with the storyboard integration that had attracted us seemingly not an option, and the steep learning curve still in mind, we began to fear that Typhoon was too complicated for our current needs. We went back to Google.
Attempt #2: Blindside
Looking for something completely different than Typhoon we next came upon Blindside, which is a small library which appealed to us because it isn’t trying to do too much, and is very simple to use. The sort of thing you can sit down and read through within an hour and appreciate exactly how it works. Each class that requires dependency injection is asked to implement one or two class methods which describe its dependencies. Then an injector object, called a “module” in Blindside, can be asked to bind, or associate, objects with a given identifier, which is essentially a string.
It’s mostly explicit, although the use of “magic” strings to relate dependencies to one another quickly began to rankle as this approach doesn’t support navigation or automated refactoring within IDEs like Xcode.
Adding sometimes hefty bsInitializer methods to each injected class also became tiresome, as ideally we wouldn’t want the classes to be responsible for this code, and aware of these special strings that dictate their dependencies. It made us worry that if we wanted to one day move to another framework, we faced making changes to hundreds of classes to untwine them from Blindside.
For these reasons we moved onto another library.
Attempt #3: Objection
As the second most popular library and one I’d heard of from colleagues, we next took a look at Objection. Objection is similar to Blindside in that it takes a different approach to Typhoon and uses “annotation” based dependency resolution. Objection also supports a rough equivalent of Typhoon’s “assembly” classes, called “modules”, which are effectively factories where objects can be bound in various ways: by protocol, class, or even by a block that is invoked when the object is required, which is an effective way to do configuration on demand. This was an approach that suited those dependencies of ours that were created asynchronously.
One really nice feature of Objection is the way you can derive an injector from another. So we could create one injector for the initial app load, then once more services were available we could create another injector from the first which maintained the initial set of services while adding in the new ones. Objection injectors are also able to take an existing, uninjected object, and inject its dependencies into it – useful for performing DI on objects whose lifecycle we don’t control, like view controllers within storyboards.
Initially we liked this: it was simple to understand and quick to integrate, and occupied the middle ground between Typhoon and Blindside. Ultimately though we still weren’t happy because occasionally we’d need to use the same untyped “magic string” approach employed by Blindside, which meant a lack of compile-time checking, implications for refactoring, etc.
It also meant needing to inconsistently augment classes with macros, which somehow seemed worse than doing so all the time. We found our code was littered with calls to “getObject” to retrieve a dependency from a module – which isn’t exactly what DI should be about. Objection is incredibly flexible, but we felt that that flexibility was coming at a price. There were also concerns about ongoing maintenance of the project.
Then a thought occurred – what if we could take the staggered derived injector approach we’d implemented with Objection’s modules and instead do the same thing with Typhoon’s assemblies?
Attempt #4 – Typhoon (again)
We went back to Typhoon. Having accepted that we couldn’t use the storyboard integration, we instead set up a few assemblies – an initial ApplicationAssembly object, for the dependencies we need at launch time, and then an assembly per coordinator. The assembly itself is injected into the coordinator, so it can use it if it needs to. Most assemblies contain dependencies that are sourced from two places – either they’re pre-existing objects that are set directly on the assembly via plain Objective-C properties, or they’re Typhoon methods which return objects after activation. Something like this:
So even though Typhoon doesn’t support a way to directly derive a new injector from another, it’s trivial for us to do it using Typhoon itself, so our coordinator assemblies are created from other assemblies.
This is all mostly handled within the assembly objects which keeps our coordinators nice and clean. The next example shows how we instantiate the split view in our app once the data and search controllers have been created asynchronously:
That method would previously have involved a lot of object instantiation and wiring, so this is a big improvement.
The last thing we needed to do was inject our dependencies into storyboarded view controllers. We did this by using Peter Steinberger’s brilliant Aspects library to hook the prepareForSegue:sender: method of UIViewController and inject the destination view controller:
It’s simple but effective. I recently came across this article which doesn’t use an injector as such but does use method swizzling to allow coordinators and storyboards to exist together, and is something we might explore in future.
After trying a few different libraries we came to the conclusion that there’s no “right” way to do what we wanted to do. Depending on your feelings regarding IDE integration, code completion, purity of classes, APIs and so on, any one of these libraries (and the others we didn’t try) will likely do a great job. For the moment we’re happy with Typhoon, happy to have cut the amount of code in our coordinators down to a minimum, and happy to have finished refactoring the core of the app so we can get back to developing new, exciting features with our injected objects and services.
If you have any questions feel free to contact me on Twitter – @craigmarvelley