This blog post introduces Swinject, a dependency injection framework for Swift. Swift 2 will come with protocol extension and encourage protocol oriented programming. In addition, Xcode 7 will introduce UI testing. In this context, it is getting more important to decouple components of an app by protocols. The typical pattern of the decoupling is called dependency injection.
Dependency Injection
Let’s start with an example. You are going to develop an app that lists current weather of some locations like the screenshot image below. The weather information will be received from a server through an API, and the data are used to present in the table view. Of course you will write unit tests. According to the screenshot, the tests will expect that the weather in Montreal1 should be “Clouds”, in Moscow “Clear” and in Los Angeles “Clouds”, but wait, if you write test code like that, will the tests pass tomorrow too? They will rarely pass because the weather changes.
The problem here is that the parts of network access and data processing are coupled. In other words, the data processing depends on the network access. If the dependency is hard-coded, it is difficult to write unit tests around the dependency. To solve the problem, the dependency should be passed from somewhere else. This is the dependency injection (DI) pattern. The external code provides dependencies to the client code. The injector is called DI container or simply container2.
Swinject
Swinject is a lightweight dependency injection framework written in Swift to use with Swift. The framework APIs are easy to learn and use because of the generic type and first class function features of Swift. Swinject is available through CocoaPods or Carthage.
Installation with CocoaPods
To install Swinject with CocoaPods, add the following lines to your Podfile
3.
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!
pod 'Swinject', '~> 0.2.0'
Then run pod install
command. For details of the installation and usage of CocoaPods, visit its official website.
Later in the example app, we will use CocoaPods to install Swinject.
Installation with Carthage
To install Swinject with Carthage, add the following line to your Cartfile
4.
github "Swinject/Swinject" ~> 0.2
Then run carthage update
command. For details of the installation and usage of Carthage, visit its project page.
Basics
Before continuing to details of the example app, let me introduce the basics of dependency injection with Swinject. Its project has a playground and it is easy to try dependency injection with Swinject. Download the source code or clone the project, and open the project file to use the playground.
Without Dependency Injection
Let’s say we are writing a game to play with animals. First we will write the program without dependency injection. Here is Cat
class to represent an animal,
class Cat {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "Meow!"
}
}
and PetOwner
class has an instance of Cat
as a pet to play with.
class PetOwner {
let pet = Cat(name: "Mimi")
func play() -> String {
return "I'm playing with \(pet.name). \(pet.sound())"
}
}
Now we can instantiate PetOwner
to play.
let petOwner = PetOwner()
print(petOwner.play()) // prints "I'm playing with Mimi. Meow!"
This is great if everyone is a cat person, but in reality some are dog persons. Because the instantiation of a Cat
is hard-coded, PetOwner
class depends on Cat
class. The dependency must be decoupled to support Dog
or other classes.
With Dependency Injection
Now is the time to start taking advantage of dependency injection. Here we are going to introduce AnimalType
protocol to get rid of the dependency.
protocol AnimalType {
var name: String { get }
func sound() -> String
}
Cat
class is modified to conform the protocol,
class Cat: AnimalType {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "Meow!"
}
}
and PetOwner
class is modified to get an AnimalType
injected through its initializer.
class PetOwner {
let pet: AnimalType
init(pet: AnimalType) {
self.pet = pet
}
func play() -> String {
return "I'm playing with \(pet.name). \(pet.sound())"
}
}
Now we can inject the dependency to AnimalType
protocol when a PetOwner
instance is created.
let catOwner = PetOwner(pet: Cat(name: "Mimi"))
print(catOwner.play()) // prints "I'm playing with Mimi. Meow!"
If we have Dog
class,
class Dog: AnimalType {
let name: String
init(name: String) {
self.name = name
}
func sound() -> String {
return "Bow wow!"
}
}
we can play with a dog too.
let dogOwner = PetOwner(pet: Dog(name: "Hachi"))
print(dogOwner.play()) // prints "I'm playing with Hachi. Bow wow!"
So far, we injected the dependency of PetOwner
by ourselves, but if we get more dependencies as the app evolved, it is harder to maintain dependency injection by hand. Let’s introduce Swinject to manage the dependencies here.
To use Swinject, add the following line to a playground or source code.
import Swinject
Then create an instance of Container
and register the dependency.
let container = Container()
container.register(AnimalType.self) { _ in Cat(name: "Mimi") }
container.register(PetOwner.self) { r in
PetOwner(pet: r.resolve(AnimalType.self)!)
}
In the code above, we told the container
to resolve AnimalType
to a Cat
instance named “Mimi”, and PetOwner
to an instance with an AnimalType
as a pet resolved by the container
. The resolve
method returns nil if the container cannot resolve an instance, but here we know AnimalType
is already registered and force-unwrap the optional parameter.
We have got the configured container. Let’s get an instance of PetOwner
from the container
.
let petOwner = container.resolve(PetOwner.self)!
print(petOwner.play()) // prints "I'm playing with Mimi. Meow!"
It is so simple to configure a Container
and to retrieve a resolved instance with its dependencies injected.
Conclusion
The concept of dependency injection has been introduced with the scenario to write unit tests for the weather app, and its basic use case has been demonstrated. With Swinject, it is easy to configure the dependencies and to get instances with the dependencies resolved. In the next blog post, we will see how to use Swinject with unit tests in the example weather app.
- The cities listed in the example app are the summer Olympic host cities since 1976. [return]
- DI container is also called assembler, provider, builder, spring or injector. [return]
- Specify version 0.1.0 if you use Xcode 6.4. Version 0.2 is required for Xcode 7 beta. [return]
- Specify version 0.1 if you use Xcode 6.4. Version 0.2 is required for Xcode 7 beta. [return]