As already mentioned in my previous post about Dependency Injection in Swift I think that DI is essential for a maintainable and testable code base. I wasn’t completely happy with my previous solutions cause you had to write a lot of boilerplate code and explicit methods. Oliver Eikemeier gave some nice talks at the Cocoaheads and Swift.berlin meetups in Berlin last year about modularization and architecture of apps including a way better solution for DI, that I adapted to my needs and want to share with you here.

It would be nice if we had just one inject() method on a central ServiceLocator and Swift would infer which object it should return via type inference and a configuration table inside the ServiceLocator, which can be configured with simple register() calls.

Swift 2.0 introduced the new ObjectIdentifier struct with which we get something hashable from a plain type in Swift. We can use this as a key in a dictionary now. That’s just the part that I didn’t find when I wrote my last post!

So how does this new ServiceLocator look like?

class ServiceLocator {
  private var registry = [ObjectIdentifier:Any]()

  static var sharedLocator = ServiceLocator()

  // MARK: Registration

  func register<Service>(factory: () -> Service) {
    let serviceId = ObjectIdentifier(Service.self)
    registry[serviceId] = factory
  }

  static func register<Service>(factory: () -> Service) {
    sharedLocator.register(factory)
  }

  // MARK: Injection

  static func inject<Service>() -> Service {
    return sharedLocator.inject()
  }
	
  func inject<Service>() -> Service {
    let serviceId = ObjectIdentifier(Service.self)
    if let factory = registry[serviceId] as? () -> Service {
      return factory()
    } else {
      fatalError("No registered entry for \(Service.self)")
    }
  }
}

You can now register a service and inject it like this:

ServiceLocator.register { MyService() }

let myService: MyService = ServiceLocator.inject()

It’s important to consider that the ObjectIdentifier is very specific. This means if you want to register implementations of a certain protocol you need to cast your implementation instance to that protocol during registration:

ServiceLocator.register { MyServiceImpl() as MyServiceProtocol }

let myService: MyServiceProtocol = ServiceLocator.inject()

Also optionals and implicit unwrapped optionals create another ObjectIdentifier than the plain types. Therefore I recommend to avoid optionals and implicit unwrapped optionals in the ServiceLocator so that you only register and inject “clean” types.

You can extended the ServiceLocator by a registerSingleton(singletonInstance: Service) method to avoid the typical sharedInstance() implementations and make it more explicit which objects will be recreated for every inject() call and for which you will always get the same:

func registerSingleton<Service>(singletonInstance: Service) {
  let serviceId = ObjectIdentifier(Service.self)
  registry[serviceId] = singletonInstance
}

func inject<Service>() -> Service {
  let serviceId = ObjectIdentifier(Service.self)
  if let factory = registry[serviceId] as? () -> Service {
    return factory()
  } else if let singletonInstance = registry[serviceId] as? Service {
    return singletonInstance
  } else {
    fatalError("No registered entry for \(Service.self)")
  }
}

Of course you need one point where you register all your services at the ServiceLocator. One good point would be the AppDelegate, but this can become a bit tedious if you have a large app. Therefore I’m using modules for each part of my app and only register these modules with the ServiceLocator in the AppDelegate. This helps to keep domain logic close together, improves modularization and makes your app scalable.

protocol ServiceLocatorModul {
  func registerServices(serviceLocator: ServiceLocator)
}

extension ServiceLocator {
  func registerModules(modules: [ServiceLocatorModul]) {
    modules.forEach { $0.registerServices(self) }
  }
}

class MyModul: ServiceLocatorModul {
  func registerServices(serviceLocator: ServiceLocator) {
    serviceLocator.register { MyService() }
    ...
  }
}

Though, you need to be a bit careful in which order you register the dependencies and what depends on what: There’s no solution for cyclic dependencies here!

In unit tests you can easily register your mock implementations for injection. It’s also recommended to wipe the registry of the ServiceLocator before every test to ensure a clean state and avoid injection of real implementations by accident.

You can find my default ServiceLocator including a CocoaDefaultModul, which includes some basic iOS singletons, in this Gist.

Of course there’s a lot of things you could add to such a ServiceLocator:

  • Check that it’s only used in initializers - Oliver did that.
  • Add identifier strings to use multiple different registrations for one type - like Spring in Java does with @Qualifier annotations.
  • Use multiple ServiceLocator instances in your app. Each one for a specific part. You might want to drop the sharedLocator then and pass specific locators around.

Because of all this variants, differing personal preferences and it’s actual simplicity I won’t make a pod/lib out of this and leave it as an exercise to the reader to copy the parts you like for your project ;-).

Thanks again @ Oliver for his talks and his implementation of a ServiceLocator, which I just modified to my needs here. You can find that in his demo project - it’s called ZContainer there.