Introducing Cleanse: A Lightweight Dependency Injection Framework For Swift
Cleanse is a pure Swift dependency injection library.
Written by Mike Lewis.
Dependency Injection for All (Mobile Devices)
Several years ago, I was introduced to dependency injection(DI) working on a Java service here. Our Java “Service Container” is built on top of Guice. After a small learning curve, it clearly became one of those technologies I couldn’t live without. DI enables software to be loosely coupled, more testable while requiring less annoying boilerplate code.
After working on a couple Java services, it was time for me to go back to iOS to build our lovely Square Appointments App in Objective-C. Moving to Objective-C meant giving up the power DI frameworks such as Guice and Dagger. Yes, there were and still are a couple DI implementations for Objective-C, but we felt they lacked safety, excessively used the Objective-C runtime, or were just too verbose to configure. Well, that didn’t stop us. We came up with a Frankensteinian solution that used LibClang and generated code based on “Annotations”. It was modeled after Dagger 1 and gave compile-time checking and several other benefits.
About a year ago, we started adopting Swift. Having a DI library based on Objective-C started to show its weaknesses, even after we added support for creating modules with swift. We couldn’t inject things that didn’t bridge to Objective-C such as structs or Observables. The code generation solution required a bit of Xcode trickery such as maintaining custom build rules and having to manually touch files to trigger recompilation. We didn’t want to give up DI though since we also hate writing boilerplate code!
Enter Cleanse
In an ideal world, we’d have implemented something like Dagger 2 for Swift. Unfortunately, Swift is lacking tooling such as annotation processors, annotations, Java-like reflection library, etc. Swift does, however, have an incredibly powerful type system.
We leverage this type system to bring you Cleanse, our dependency injection framework for Swift. Configuring Cleanse modules may look very similar to configuring Guice modules. However, a lot of inspiration has also come from Dagger 2, such as components and lack of ObjectGraph/Injectortypes which allows for unsafe practices. This lets us have a modern DI framework with a robustfeature set that we can use today!
A Quick Tour
We made a small example playground that demonstrates wiring up an HTTP client to make requests to GitHub’s API.
Unlike the two de facto Java DI frameworks, Dagger and Guice, which support several types ofbindings and injections, Cleanse operates primarily on Factory injection. In this context, factory is just a function type that takes 0..N arguments and returns a new instance. Conveniently, if one has a GithubListMembersServiceImpl type, GithubListMembersServiceImpl.init, is a factory for GithubListMembersServiceImpl, which makes it almost equivalent to Constructor Injection.
Let’s say we have a protocol defined which should list the members of a GitHub organization.
**protocol** GithubListMembersService {
**func** listMembers(organizationName: String, handler: [String] **->** ())
}
And we implement this protocol as GithubListMembersServiceImpl
**struct** GithubListMembersServiceImpl : GithubListMembersService {
*// We require a github base URL and an NSURLSession to perform our task*
let githubURL: TaggedProvider<GithubBaseURL>
let urlSession: NSURLSession
/// Lists members of an organization (ignores errors for sake of example)
func listMembers(organizationName: String, handler: [String] -> ()) {
let url = githubURL.get().URLByAppendingPathComponent("orgs/\(organizationName)/public_members")
let dataTask = urlSession.dataTaskWithURL(url) { data, response, error in
guard let data = data, result = (try? NSJSONSerialization.JSONObjectWithData(data, options: [])) as? [[String: AnyObject]] else {
handler([])
return
}
handler(result.flatMap { $0["login"] as? String })
}
dataTask.resume()
}
}
We want this implementation to be provided whenever a GithubListMembersService is requested. To do this, we configure it in a Module. Modules are the building blocks for configuring Cleanse.
**struct** GithubAPIModule : Module {
**func** configure**<**B : Binder**>**(binder binder: B) {
*// Configure GithubMembersServiceImpl to be the implementation of GithubMembersService*
binder
.bind(GithubListMembersService.self)
.asSingleton()
.to(factory: GithubListMembersServiceImpl.**init**)
// While we're at it, configure the github Base URL to be "https://api.github.com"
binder
.bind()
.tagged(with: GithubBaseURL.self)
.to(value: NSURL(string: "https://api.github.com")!)
}
}
You may have noticed that GithubListMembersServiceImpl requires an NSURLSession. To satisfy that requirement, we’ll need to configure that as well. Let’s make another module:
**struct** NetworkModule : Module {
**func** configure**<**B : Binder**>**(binder binder: B) {
*// Make `NSURLSessionConfiguration.ephemeralSessionConfiguration` be provided*
*// when one requests a `NSURLSessionConfiguration`*
binder
.bind()
.asSingleton()
.to(factory: NSURLSessionConfiguration.ephemeralSessionConfiguration)
// Make `NSURLSession` available.
// It depends on `NSURLSessionConfiguration` configured above (`$0`)
binder
.bind()
.asSingleton()
.to {
NSURLSession(
configuration: $0,
delegate: nil,
delegateQueue: NSOperationQueue.mainQueue()
)
}
}
}
We can assemble these two modules in a Component. A Component is essentially a Module which also declares a Root type for an object graph. In this case, we want our root to be GithubListMembersService.
**struct** GithubListMembersComponent : Component {
*// When we build this component we want `GithubListMembersService` returned*
**typealias** Root **=** GithubListMembersService
func configure<B : Binder>(binder binder: B) {
// Install both the modules we have made
binder.install(module: NetworkModule())
binder.install(module: GithubAPIModule())
}
}
Now its time to build the component and get our GithubListMembersService! We call *build() *on an instance of our component. This returns the Root type, GithubListMembersService.
**let** membersService **=** try**!** GithubListMembersComponent().build()
If there are validation errors constructing our object graph, they would be thrown from the build() method.
Now, let’s see who the members of Square’s GitHub org are:
membersService.listMembers("square") { members **in**
print("Fetched \(members.count) members:")
for (i, login) in members.enumerate() {
print("\(i+1).\t\(login)")
}
}
A more detailed getting started guide can be found in the README or by taking a look at ourexample app.
The Code
One can check out Cleanse on GitHub.
Cleanse is a work in progress, but we feel it has the building blocks for a very powerful and developer friendly DI framework. We’d like to encourage community involvement for developing more advanced features (e.g. Subcomponents like in Dagger 2). Its current implementation supports both Swift 2.2 and the open source version of Swift 3.
We’re in the process of completely migrating Square Appointments App to Cleanse for all our DI needs in the near future. Expect to see exciting new features, improvements, more documentation, examples, and maybe even some more articles over the coming weeks and months. Mike Lewis (@MikeLewis) | Twitter *The latest Tweets from Mike Lewis (@MikeLewis). maker of stuff. kitten enthusiast. iOS hacker/troublemaker at @square…*twitter.com