SwInjecting a large project

Swift   swinject   di   iOS  

If you know me, you'll know I'm a bit of a fan of Dependency Injection frameworks and when it comes to Swift projects, my goto framework is Swinject.

The documentation on Swinject's Github page is pretty good so I'd suggest reading that if you're not familiar with what it can do. However one area it doesn't cover so well is using it on larger code bases. So this post is about showing you some of the tricks I've learned that can make life a lot easier on larger projects.

Setting up

First lets look at setting up Swinject in your project. Using a real project as an example is impractical because of the amount of code involved, so I'll use a simple set of classes as if they were part of a large code base to show how these tricks can be applied.

Containers

Lets talk containers. Swinject uses the concept of a container to manage objects. To quickly summarise, containers:

  • Store factory closures which it uses to create objects when needed.
  • Act as a resolver for locating any dependencies required to create an object based on a passed type and argument types, or optionally a name stored with the factory closure.
  • Store the finished object for injecting into other instances.
  • Manage the lifecycle of the objects it creates.

In a large project I'd suggest using multiple containers because it allows you to group registrations into logical layers. You can base it on your app's architecture, functional themes, or whatever suites you. For example, lets setup two containers - one dedicated to various backend services such as network code, and one dedicated to the UI.

When using multiple containers I highly recommend setting up a parent/child hierarchy as it helps to manage the dependency resolving. In our example, we'll set the service container as the parent of the UI container. This lets the UI container resolve objects from the service container, but not visa versa. So view controllers can have services injected into them, but services cannot have UI objects injected.

To support this, I use two simple protocols:

import Swinject

protocol Registerable {  
    static func register(inContainer container:Container)
}

public protocol DependencyInjectionConfig {  
    static var container: Container { get set }
    static var managedClasses: [Registerable.Type] { get }
    static func configure()
}

Now lets create our two containers using the DependencyInjectionConfig protocol:

import Swinject

strut UIContainer: DependencyInjectionConfig {  
    static var container = Container(parent: ServiceContainer.container)
}

struct ServiceContainer: DependencyInjectionConfig {  
    static var container = Container()
}

Where to register

The simple examples in Swinject's doco show registrations being done in the same block of code. That works well for a small projects, but in a larger ones it can grow into a large block of unmanageable code quickly.

I prefer locating the code that registers within the class or struct it's registering. Thus each class or struct effectively registers itself. This keeps the registration code small, localised and more manageable.

The first part of doing this is add an extension to the DependencyInjectionConfig protocol to implement a common configure() function:

public extension DependencyInjectionConfig {  
    static func configure() {
        self.managedClasses.forEach { 
            $0.register(inContainer: self.container)
        }
    }
}

Essentially it reads a list of classes from the managedClasses property and loops through them, calling the register(...) method on each and passing the current container as an argument.

Next add managedClasses properties to return a list of the relevant classes:

import Swinject

strut UIContainer: DependencyInjectionConfig {  
    static var container: Container = Container(parent: ServiceContainer.container)
    static var managedClasses: [Registerable.Type] {
        return [
            MyViewController.self,
        ]
    }
}

struct ServiceContainer: DependencyInjectionConfig {  
    static var container: Container = Container()
    static var managedClasses: [Registerable.Type] {
        return [
            NetworkService.self,
            DeviceService.self,
        ]
    }
}

Now that the containers are setup, all that's needed is to implement the Registerable protocol on each class:

import Swinject

protocol Device {}

class DeviceService: Device, Registerable {

    class func register(inContainer container:Container) {
        container.register(Device.self) { _ in
            DeviceService()
        }
    }

    // Rest of definition ...
}
import Swinject

protocol Network {}

class NetworkService: Network, Registerable {

    class func register(inContainer container:Container) {
        container.register(Network.self) { resolver in
            NetworkService(device:resolver.resolve(Device.self)!)
        }
    }

    private let device: Device

    init(device: Device) {
        self.device = device
    }

    // Rest of definition ...
}
import Swinject  
import SwinjectStoryboard

class MyViewController: UIViewController, Registerable {

    private var network: Network!
    private var device: Device!

    class func register(inContainer container:Container) {
        container.storyboardInitCompleted(MyViewController.self) { resolver, controller in
            controller.network = resolver.resolve(Network.self)!
            controller.device = resolver.resolve(Device.self)!
        }
    }

    // Rest of view controller definition ...
}

Note the different style of registration in the MyViewController class. That's because it's a view controller instantiated by a UIStoryboard rather than a Swinject factory closure.

This is required when using Swinject's SwinjectStoryboard extension. This swizzles itself into the UIStoryboard class so it's called after the storyboard instantiates an object so that dependencies can be resolved.

Starting up

Now lets consider the way an app starts up. If the initial view controller of your app has dependencies which need resolving, Swinject will be called before the app delegate's methods. So the containers have to be setup earlier in the lifecycle or the app or it will crash due to the containers not being present or dependencies not being available.

Firstly I setup up the service container in the app delegate's init():

class AppDelegate: UIApplicationDelegate {  
    override init() {
        super.init()
        ServiceContainer.configure()
    }
}

Secondly we need to setup the UI container. Luckily there's a convenient hook for setting it up. It's done through an extension on SwinjectStoryboard that implements the setup() method:

import Swinject  
import SwinjectStoryboard

extension SwinjectStoryboard {  
    @objc static func setup() {
        UIContainer.configure()
        SwinjectStoryboard.defaultContainer = UIContainer.container
    }
}

setup() will be called automatically on startup so there's nothing more needed. All we need to do is setup the container, then set it as the defaultContainer so SwinjectStoryboard will use it to resolve dependencies.

That's it. Everything is now in place and ready to go.

Solutions

Now lets look at some specific situations you might encounter in a larger project and how they can be addressed using Swinject.

Sequential process data

It's not unusual for larger app's to have processes that spans multiple steps. For example a user registration process might be as simple as one view controller, or a complex flow of fixed and optional view controllers backed by service objects. One of the issues when designing such processes is how to pass data between steps and usually developers implement prepare(for:sender:) on the view controller's to pass data from one view controller to the next along the sequence.

This works fine when each view controller needs the data and the order of view controllers is clear. But sometimes there are view controllers in the process that don't need the data or perhaps it's not the view controllers that use it. So how do we pass the data?

The default solutions can be used, but you'll have to add a lot of boilerplate code simply to move data to where it's needed. Which means view controllers containing code managing data the view controller has no interest in. Swinject can help you avoid such boilerplate and to a degree, decouple not only the view controllers from each other, but also any backing service objects which may need the data as well.

Here's an example class that contains the data for a process showing how this can be done:

class UserRegistationFormData: Registerable {  
    class func register(inContainer container:Container) {
        UserRegistationFormData()
    }.inObjectScope(.weak)

    // Struct properties ...
}

The .inObjectScope(.weak) call is the trick. Object scopes define how Swinject manages the objects it creates and in most situations the default is fine, but specifying a weak scope tells Swinject to treat the registration using Swift's weak rules.

So the first time Swinject resolves the object at the start of the process, it will create a new instance and store it in the container using a 'weak var ...'. Then as long as there is at least one external strong reference to the object, Swinject will continue to resolve dependencies using the original instance. Once the process ends and all the view controllers and objects dealloc. Swinject's reference will nil out it's internal reference as well. Then the next the process runs, a new one will be created again and off we go.

The real beauty of this is that only the view controllers and objects that need the data need to resolve it and those that don't avoid the boilerplate usually used to pass the data around.

The only one rule when using Swinject's weak scope - the objects must be classes. Using weak structs are registrable, but don't work because resolving a struct will always give you a new copy.

Contextual groups of objects

Sometimes you have a bunch of objects which are related and you want to manage their lifecycle as a whole. For example, a group of objects which represent a banking or insurance calculation. In these situations, you want the ability to create and wire them together, use them, then later on, clear them out to make way for a new calculation.

Swinject custom scopes can solve this. Essentially a custom scope give you the ability to manually control the lifecycle of any objects created in that scope rather than depending on Swinject to do it. ie. You decide when to dealloc them.

Using a custom scope is simple. Firstly, you create it by extending ObjectScope like this:

extension ObjectScope {  
    public static let insuranceCalc = ObjectScope(storageFactory: PermanentStorage.init)
}

Then you register using the custom scope just like any other scope:

class PropertyValueCalc: Registerable {  
    class func register(inContainer container:Container) {
        PropertyValueCalc()
    }.inObjectScope(.insuranceCalc)
}

And once you've finished with the calculation, you simply ask Swinject to reset the scope, which clears all stored references:

ServiceContainer.container.resetObjectScope(.insuranceCalc)  

Killing those logs

When Swinject fails to resolve an object, it quite usefully logs how it was asked to resolve it and all the registrations in the container so that you can compare them to see what went wrong.

This is useful when you actually mess something up, but when using SwinjectStoryboard, all view controllers and objects being instantiated by a UIStoryboard are passed to Swinject assuming they have dependencies that need injecting. But if they aren't registered because they don't have any dependencies, Swinject assumes you've got something wrong and logs the container, assuming you'll want to know.

This can get quite annoying and it would be great if Swinject had the ability to specify when storyboard based objects don't need resolving, but it doesn't. Instead there's a single hook where you can set a custom function to do logging, or set nil to do none.

With a little work though, we can set a function that logs when a view controller or object fails to resolve, but swallows the logging of the container to keep you logs small and easy to read. Here's what it looks like:

Container.loggingFunction = {

    // Find the text that contains the missing registration.
    if let startOfMissingRegistration = $0.range(of: "Swinject: Resolution failed. Expected registration:\n\t")?.upperBound,
        let startOfAvailableOptions = $0.range(of: "\nAvailable registrations:")?.lowerBound {

        let missingRegistration = $0[startOfMissingRegistration ..< startOfAvailableOptions]

        // Ignore all reports for UIKit classes and
        // also exit if any of your classes are not registered
        if missingRegistration.contains("Storyboard: UI")
            || missingRegistration.contains("Storyboard: MyViewController") {
            return
        }

        // Print the missing registration.
        print("Swinject failed to find registration for \(missingRegistration)")
        return
    }

    // Some other message so just print it.
    print($0)
}

Ok, so that's great at removing the list of all the registrations from the logs, but then what if you do have a genuine issue and you want to see whats in the container?

It's quite easy to handle. Simply add a break point somewhere in the controller that should have had dependencies resolved, then when the debugger pauses, print out the container like this:

po ServiceContainer.container  

Simple.


Comments powered by Disqus