Pragmatic Dependency Injection in Swift - Part Two

Last time on Pragmatic Dependency Injection in Swift, we discussed the horror of global variables and the duplicity of singletons, globals in disguise. Dependency Injection (DI) can help us avoid these pitfalls, but how do we implement DI in Swift without incurring a prohibitive amount of overhead? To find out, we join our regularly scheduled blog post, already in progres...

DI in Swift

So, DI is awesome. Now I want to use this in my Swift project. Let’s consider a standard mobile app. You might have a whole list of objects that had been singletons in the past. Some devs like to call these Manager objects.

  • LocationManager — keeps track of GPS coordinates
  • DataManger — handles data storage
  • ApiManager — coordinates communication to a server
  • LogManager — aggregates debug/error messages to multiple destinations
  • NotificationManager — receives and interprets APNS notifications

And that’s just the tip of the iceberg. In a current project, I’ve got fifteen of these manager style objects. With DI, we need to pass these down through our object hierarchy, but all those init() methods start to look a bit cumbersome with fifteen arguments. Maybe we could just pass what’s needed? That’s a good idea, except the table view cell ten levels deep in the view controller hierarchy suddenly needs access to the LocationManager. You’ll have to create a path to pass it all the way down through the full view controller tree.

So we’ll need to pass everything down, but clearly it’s too cumbersome to do that for each Manager. Also, if you add a new one, you’d need to update a ton of code. We need some kind of Manager holder to encapsulate these into one object that can easily be passed through our application. I like to think of it like a drink caddy that holds multiple bottles of sweet cherry goodness.

Now that we’re moving away from singletons, we’ll change the name from Manager to Controller. Our container object is really a Controller for Controllers, so we’ll call it a MetaController.

Show Me the Code!

Let’s talk about semantics for just a moment. When I say we’re moving away from singletons, I mean the “singleton pattern” of an object that guarantees that it’s only instantiated once and accessed through a static method, like a global variable. In most cases, our Controller objects in the app are single instances, but they are created like normal objects and explicitly passed around via the MetaController. Nothing prevents you from creating more than one of the controller objects, but it’s just not necessary in most cases. If you do need more than one, you have that option.

Back to the MetaController. Here’s what it looks like in basic form.

class MetaController {
    var locationController: LocationController? = nil
    var logController: LogController? = nil
    var apiController: ApiController? = nil 
    var dataController: DataController? = nil 
    var notificationController: NotificationController? = nil 
}

And that’s it! Okay, it’s just the beginning. Like other components in the system, the controllers themselves often need access to the other controllers in the system. For example, the ApiController needs access to the LogController to log network issues connecting to the server. To make this more convenient, we add a little didSet action in the MetaController to pass the MetaController itself to the payload controllers.

class MetaController {
    var locationController: LocationController? = nil { didSet { passController(locationController) }}
    var logController: LogController? = nil { didSet { passController(logController) }}
    var apiController: ApiController? = nil { didSet { passController(apiController) }}
    var dataController: DataController? = nil { didSet { passController(dataController) }}
    var notificationController: NotificationController? = nil { didSet { passController(remoteNotificationController) }}

    func passController(target: AnyObject?) {
        if var target = target as? MetaControllerConsumer {
            target.metaController = self
        }
     }
 }

Here’s our first glimpse of the MetaControllerConsumer protocol. This swift protocol is adopted by every object in our system that uses the MetaController. The basic definition is super simple and just declares that the consumer will have a place to store the MetaController

protocol MetaControllerConsumer {
    var metaController: MetaController? { get set }
}

And that’s all that’s necessary, but we want to make this easy to use as well. Every time we access the dataController, it would be a pain to type metaController?.dataController. The great thing about Swift protocols is that you can add default implementations for protocol compliance. This lets us expand the properties required by the protocol, but also define default implementations of synthetic properties to fulfill the protocol requirements. That’s a mouthful. Here’s what that looks like:

protocol MetaControllerConsumer {
    var metaController: MetaController? { get set }
    var locationController: LocationController? { get }
    var logController: LogController? { get }
    var apiController: ApiController? { get }
    var dataController: DataController? { get }
    var notificationController: notificationController? { get }
}

// default protocol implementation to return the sub controllers from the meta controller
extension MetaControllerConsumer  {
    var locationController: LocationController? {  return metaController?.locationController }
    var apiController: ApiController? { return metaController?.apiController }
    var dataController: DataController? { return metaController?.dataController }
    var userController: UserController? { return metaController?.userController }
    var remoteNotificationController: RemoteNotificationController? { return metaController?.remoteNotificationController }
}

A MetaControllerConsumer still only has to declare a real property called metaController to hold the container, but can now access the sub-controllers like dataController directly via the default implementation of the computed properties.

We can add more default implementations for other convenience operations too. Logging is a good candidate for these sorts of shortcuts.

extension MetaControllerConsumer {    // Logging shortcuts
    func debugLog(message: String, _ path: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line){
        logController?.debugLog(message, path, function, line)
    }
    func errorLog(message: String, _ path: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line){
        logController?.errorLog(message, path, function, line)
    }
}

With this in place, the logging functions debugLog() and errorLog() are now available in every MetaControllerConsumer. By leveraging Swift protocols like this, it’s easy to add general methods in one place that are accessible everywhere.

BTW, the #file, #function, and #line arguments in these functions are tokens that the compiler recognizes and replaces with the current filename, function name, and line number. Adding a debug statement like debugLog("Bazinga!") can print out

LocationController.swift (line 34): updateLocation(): Bazinga!

which is pretty handy.

Another useful function allows us to quickly pass the MetaController to another object, using the MetaController itself to first check if the target object can consume it.

extension MetaControllerConsumer {
    func passMetaController(target: AnyObject?)         
        metaController?.passController(target)
    }
 }

This is especially useful when programmatically instantiating a new object in the code or configuring a child view controller in prepareForSegue().

We load up the MetaController early in the app bootstrap process (from the app delegate’s didFinishLaunchingWithOptions) and pass it down through our object hierarchy. Initialization is really straightforward.

metaController = MetaController()
metaController?.logController = LogController()
metaController?.locationController = LocationController()
metaController?.apiController = ApiController()
metaController?.dataController = DataController()
metaController?.notificationController = NotificationController()

The MetaController is also handed to the root view controller which handles propagating it down through the view controller ranks.

Testing

With the MetaController construct, we reap all the normal DI benefits for testing. For each test, we simply construct a new MetaController, add the controllers that the test needs (replacing with “Fake” subclasses as necessary) and go. This is a little bit of compromise, since we need to know which sub-controllers are required by a class, but it’s a trade-off for the convenience of this proxy model of direct injection. In the strictest sense, we could still provide a fully loaded MetaController if the software under test were truly a black box. In the end, we still have a highly testable framework where we can easily satisfy dependency requirements and create robust, maintainable, explicit test suites.

Solving DI with Swift

Building an app by relying on singletons quickly leads to a mine field of Spooky Action, unintended consequences and unexpected explosions. By moving to a direct injection model, we remove the magic and hidden pitfalls of implicit dependencies and replace them with clear and explicit declarations of dependencies between the components of our app. With the MetaController model and heavy use of Swift protocols, we’re able to encapsulate the overhead of DI in a way that makes it easy and fast to apply DI to our app development process.