Dependency Inject in Roark

A few months back, I explored redoing my blog in Vapor. I still may, but that is on hiatus right now. One of the things I liked was its dependency injection framework, Service. It’s not as sophisticated as the one Microsoft provides with ASP.NET Core, but that may just mostly be due to Swift’s limited support for reflection.

Since Service is designed for server-side Swift, it a took little bit of work to get it going in an iOS project. I discussed this a bit in my previous post. Once that was done, the rest came down to application architecture.

When using DI, the composition root should be as close to the application’s entry point as possible. Normally, that would be main. However, I am using storyboards in my app, so my app doesn’t have a main function. Instead, it has its app delegate. I also want to support providers (although I don’t currently anticipate taking advantage of them), so I had to make sure I implemented the provider boot phase.

The approach I ended up using takes some inspiration from Vapor. I have a configure function that serves as my composition root. I call it from my AppDelegate’s initializer just before I call willBoot for each of the providers in my app. In application(_:didFinishLaunchingWithOptions:), I call didBoot for each as well. Additionally, I turned the default initializer into a convenience initializer to faclitate testing and conformed my AppDelegate to container.

init(config: Config, environment: Environment, services: Services, configure: CompositionRoot = configure) {
    self.config = config
    self.environment = environment
    self.services = services

    super.init()

    configure(&self.config, &self.environment, &self.services)

    do {
        try self.providers.map({ try $0.willBoot(self) }).flatten(on: self).wait()
    } catch let err {
        let log = try! self.make(Logger.self)
        log.fatal("‘willBoot’ failure during initialization: \(err)")
    }
}

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    do {
        try self.providers.map({ try $0.didBoot(self) }).flatten(on: self).wait()
    } catch let err {
        let log = try! self.make(Logger.self)
        log.fatal("‘didBoot’ failure after initialization: \(err)")
    }
    return true
}

One slight complication is that Vapor 3 is asynchronous, using SwiftNIO. Fortunately, SwiftNIO works on iOS (with some caveats). I got my EventLoop working, but I ran into a problem in my unit tests. I was testing to make sure that I implemented everything correctly, and that service registration was working as expected. To do this, I was creating a new AppDelegate for each test. This caused problems in my deinitializer. I ended up having to switch from using shutdownGracefully(error:) to syncShutdownGracefully(), which works. I’m not sure why, but it does.

deinit {
    do {
        try self.eventLoopGroup.syncShutdownGracefully()
    } catch let error {
        let log = try! self.make(Logger.self)
        log.fatal("EventLoopGroup failed to shutdown gracefully: \(error)")
    }
}

Now that I have this all working, I hope to start in on that actual database part of the app. I still plan on storing the user’s threat maps in SQLite databases. I’ve split support for reading and writing them across several projects (domain, service, and data access) to try to keep things testable (and limit temptations to reach across those boundaries).