Blog

IoC in iOS and Swift


Inversion of Control and Dependency Injection are hot topics in the .NET world. They are however rare in the iOS community, despite being powerful tools. Let’s look at a way to setup IoC in iOS.

UPDATE 2022 Since writing this, better alternatives are now available for Swift, like Dip and Swinject. Today, however, I have ditched using an engine, which often requires bootstrapping, and instead have a static IoC class that lazily resolves dependencies in a more effective way.

For those of you who are new to Inversion of Control and Dependency Injection, let’s define the world’s shortest acronym dictionary:

  • iOS = Apple’s Mobile Operating System
  • IoC = Inversion of Control
  • DI = Dependency Injection

This post targets those of you who understand the concept of IoC and are looking for a way to get it to work in iOS. For those of you who are unfamiliar with the concept of IoC, apologize me for skipping over core concepts you may not know about.

I’ll first describe my thoughts on system architecture. If you’re only interested in IoC, please skip ahead.

Why I want IoC in iOS

Being a C# developer at heart, my impression is that the concept of IoC/DI is far less known about and even less embraced by the iOS community than in more system development-oriented contexts. There are several great C# frameworks for IoC and DI, making it a breeze to setup on the .NET stack.

In iOS, though, my impression is that (and sorry for being a dick here) projects are most often hacks, with good software architecture being fairly uncommon. The fact that many developers jump on iOS development without any prior knowledge of how to build great systems, means that they have never had to think about these challenges…and end up with 99% of their code base in AppDelegate, just because that is how Paul Hegarty sets up critical code in his iTunes U iOS course.

I also learned iOS through Paul Hegarty’s iTunes U iOS course. However, I was really disappointed that he so often suggested putting code in AppDelegate. And sure enough, most iOS projects I have looked at have their AppDelegates being huge monoliths, handling a lot of the app’s core logic, defining global methods, keeping global state etc. It’s nothing less than a terrible way of writing code.

Don’t get me wrong. I am deeply impressed by the many developers who manage to tweak the iOS platform in ways I could not, providing us with amazing libraries. However, compared to the maturity level of the .NET community, the iOS community should start discussing these topics more often.

How I build things

I prefer to base most functionality on protocols. I then add implementations that can handle the task in various ways. This approach makes it easy to unit test the system, use fake implementations early on and replace implementations later on at one single place etc. etc.

I prefer abstract components that can be easily replaced at one place, with each dependency being automatically resolved. I have therefore looked for a clean way of implementing IoC in my iOS apps. I want one place where I wire up the app, to make it easy to reconfigure the app whenever I need to, and be certain that there are zero side-effects, that all parts of my app still behave correctly.

With this in mind, I think I have finally found a nice approach. I will first describe how I tried to solve these problems using an object factory, as well as the drawbacks of this approach. If you are just interested in the IoC solution, please skip this section.

Object factory, the first flawed approach

Starting out with iOS, I used to go with manually handled, static object factory classes. Every protocol I wanted to resolve had to be added to these classes, as such:

@interface ObjectFactory : NSObject

+ (id<AppNavigator>)getAppNavigator;
// etc. etc.

@end

A big drawback with this approach, is that it requires one method for each protocol that I want to resolve. It’s also static (at least the approach above), which means that classes must rely on it.

The object factory pattern also works bad for library code, since library components may require the existence of an object factory, which has to be defined in app scope, leading to dependencies pointing out of the core lib. Not good.

Say hello to CoreMeta

When I decided to solve my IoC pains for real, I found a library called CoreMeta.

As you add CoreMeta to your project, you may run into some name collisions, since CoreMeta doesn’t prefix it’s classes, protocols and categories. This means that some CoreMeta files, like Container.h and NSString+Utilities.h may collide with your own. It also includes many files that it doesn’t use.

With that said, you may have to tweak CoreMeta a bit to get it to work. It’s not a big hassle, but note that you may run into unexpected bugs when adding CoreMeta.

CoreMeta’s central class is Container (they should have named it CMContainer or anything else than the very general name Container), which is used to register implementations in various ways, then used to resolve registered classes and protocol.

CoreMeta is also smart enough to recursively resolve implementations. This means that if you have two types (A and B) and B has a property of type A, CoreMeta will automatically resolve A when you retrieve B through the container.

How I put it all together

CoreMeta is great, but I don’t want my entire app to know about it. I therefore have a general container protocol, then register CoreMeta’s Container as the type to use in my app. This makes it possible for my app to rely on a general container, rather than a specific implementation.

Furthermore, some classes will not be resolved using the container. For instance, view controllers and views are often created by storyboards or xibs. For such types, I want to be able to access the container with minimum coupling.

With all this in mind, this is how I set it up.

1. Add CoreMeta to your app project

Since CoreMeta is a library with a lot of protocols, classes and categories, you shouldn’t add it to any libraries you may have, since that would cause CoreMeta dependencies to leak throughout the entire system. Instead, add CoreMeta to your app project.

2. Create a container abstraction

To reduce coupling, add an IoC container protocol to your app project. Since the CoreMeta api is really nice, I just let the protocol reflect the parts I use:

@protocol IoCContainer <NSObject>

- (id)objectForKey:(NSString *) key;
- (id)objectForClass:(Class)classType;
- (id)objectForClass:(Class)classType cache:(BOOL)cache;
- (id)objectForClass:(Class)classType withPropertyValues:(NSDictionary *)dictionary;
- (id)objectForClass:(Class)classType usingInitSelector:(SEL)selector withArguments:(NSArray*)args;
- (id)objectForProtocol:(Protocol *)protocol;

@end

3. Make CoreMeta implement IoCContainer

In order to make it possible to register the CoreMeta Container class as the IoC container of choice, add the following to the Container class:

#import "IoCContainer.h"

@interface Container : NSObject<IoCContainer>

Since we used the Container class as template for our IoCContainer protocol, all required methods are already implemented. If you use another IoC framework, you may have to create a facade class that implements the IoCContainer protocol.

4. Global IoC awareness

At the same level as the IoCContainer protocol, add an NSObject category that lets you register your IoCContainer implementation and retrieve the registered container.

// NSObject+IoCContainer.h

#import <Foundation/Foundation.h>
#import "IoCContainer.h"

@interface NSObject (IoCContainer)

@property (nonatomic, readonly) id<IoCContainer> ioc;

- (void)registerIoCContainer:(id<IoCContainer>)ioc;

@end
// NSObject+IoCContainer.m

#import "NSObject+IoCContainer.h"

@implementation NSObject (IoCContainer)

static id<IoCContainer>_ioc;

- (id<IoCContainer>)ioc {
    return _ioc;
}

- (void)registerIoCContainer:(id<IoCContainer>)ioc {
    _ioc = ioc;
}

@end

To make the extension globally available, then add NSObject+IoCContainer.h to any prefix files you have. In Swift, add the reference to your bridging header.

In Swift, I have replaced the category with a static IoC class that has a container property that gets and sets a static variable.

// IoC.swift

import Foundation

private var _container : IoCContainer?

class IoC {
    class var container: IoCContainer {
        get { return _container! }
        set { _container = newValue }
    }
}

5. IoC container registration

Next to the AppDelegate class, create a Bootstrapper class that bootstraps the application. The one below is written in Swift and uses the IoC.container approach instead of the IoC category:

// AppBootstrapper.swift

import UIKit

class AppBootstrapper : NSObject {

    func bootstrap() {
        let container = Container.sharedContainer()
        IoC.container = container

        container.registerClass(MyClass.self, forProtocol:MyProtocol.self, cache:false)
    }
}

The class first registers the container we want to use, then registers everything needed to make the app work. Then call the method from your AppDelegate, as such:

// AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
        AppBootstrapper().bootstrap()
        ...

This approach means that an IoC container will be registered when the app starts. For unit tests, you are free to register any container you want, at any time.

6. Resolve implementations through the container

We are finally ready to try our IoC in action. For instance, let’s setup a Theme protocol that can be implemented by classes that can affects navigation bar color, search bar appearance etc.

After registering the Theme protocol and implementation in the bootstrapper, the app delegate just have to call the following at startup:

(ioc.objectForProtocol(Theme) as Theme).applyTheme()

The registered Theme implementation will be resolved, then properly applied.

Conclusion

That’s about it, hope this helps. It was written in quite a rush, so let me know if I missed some vital information, wrote some typos, have some incorrect info etc.