Dependency injection (DI) is a software design pattern that uses inversion of control (IoC) for resolving dependencies. A DI container manages the type dependencies of your system. First, you register the types that should be resolved, with their dependencies. Then you use the DI container to get instances of those types whose dependencies are then automatically resolved by the DI container. In Swinject, the Container
class represents the DI container.
Unfortunately, implementations of dependency injection often use slightly different terminology, which if not explained clearly in advance can cause confusion. In Swinject we define the following terms:
- Service: A protocol defining an interface for a dependent type.
- Component: An actual type implementing a service.
- Factory: A function or closure instantiating a component.
- Container: A collection of component instances.
Note the terms service instance and component instance mean the same thing, and are used interchangeably.
A service and its corresponding component are registered with the register
method of the container. The method takes the service type for the component and a factory method. If the component depends on another service, the factory method can call resolve
on the passed in resolver objection to "inject" the dependency. The actual underlying type of the dependency will be determined later when the component instance is created.
Here is an example of service registration:
let container = Container()
container.register(Animal.self) { _ in Cat(name: "Mimi") }
container.register(Person.self) { r in
PetOwner(name: "Stephen", pet: r.resolve(Animal.self)!)
}
Where the protocols and classes are:
protocol Animal {
var name: String { get }
}
protocol Person {
var name: String { get }
}
class Cat: Animal {
let name: String
init(name: String) {
self.name = name
}
}
class PetOwner: Person {
let name: String
let pet: Animal
init(name: String, pet: Animal) {
self.name = name
self.pet = pet
}
}
After you register the components, you can get service instances from the container by calling the resolve
method. It returns resolved components with the specified service (protocol) types.
let animal = container.resolve(Animal.self)!
let person = container.resolve(Person.self)!
let pet = (person as! PetOwner).pet
print(animal.name) // prints "Mimi"
print(animal is Cat) // prints "true"
print(person.name) // prints "Stephen"
print(person is PetOwner) // prints "true"
print(pet.name) // prints "Mimi"
print(pet is Cat) // prints "true"
Here the instances resolved by the resolve
method are unwrapped with !
because it returns nil
if no registration for the specified service type is found.
If you would like to register two or more components for a service type, you can name the registrations to differentiate.
let container = Container()
container.register(Animal.self, name: "cat") { _ in Cat(name: "Mimi") }
container.register(Animal.self, name: "dog") { _ in Dog(name: "Hachi") }
Then you can get the service instances with the registered names:
let cat = container.resolve(Animal.self, name:"cat")!
let dog = container.resolve(Animal.self, name:"dog")!
print(cat.name) // prints "Mimi"
print(cat is Cat) // prints "true"
print(dog.name) // prints "Hachi"
print(dog is Dog) // prints "true"
Where Dog
class is:
class Dog: Animal {
let name: String
init(name: String) {
self.name = name
}
}
The factory closure passed to the register
method can take arguments that are passed when the service is resolved. When you register the service, the arguments can be specified after the Resolver
parameter. Note that if the resolver is not use it can be given as _
as in the following example (this is practice in Swift):
container.register(Animal.self) { _, name in
Horse(name: name)
}
container.register(Animal.self) { _, name, running in
Horse(name: name, running: running)
}
Then pass runtime arguments when you call resolve
method. If you pass only 1 argument, use resolve(_:argument:)
.
let animal1 = container.resolve(Animal.self, argument: "Spirit")!
print(animal1.name) // prints "Spirit"
print((animal1 as! Horse).running) // prints "false"
If you pass 2 arguments or more, use resolve(_:arguments:,_:)
.
let animal2 = container.resolve(Animal.self, arguments: "Lucky", true)!
print(animal2.name) // prints "Lucky"
print((animal2 as! Horse).running) // prints "true"
Where the Horse
class is:
class Horse: Animal {
let name: String
let running: Bool
convenience init(name: String) {
self.init(name: name, running: false)
}
init(name: String, running: Bool) {
self.name = name
self.running = running
}
}
A registration of the component for a given service is stored in a container with an internally created key. The container uses the key when trying to resolve a service dependency.
The key consists of:
- The type of the service
- The name of the registration
- The number and types of the arguments
If a registration matches an existing registration with all the parts of the key, the existing registration is overwritten with the new registration.
For example, the following registrations can co-exist in a container because the service types are different:
container.register(Animal.self) { _ in Cat(name: "Mimi") }
container.register(Person.self) { r in
PetOwner(name: "Stephen", pet: r.resolve(Animal.self)!)
}
The following registrations can co-exist in a container because the registration names are different:
container.register(Animal.self, name: "cat") { _ in Cat(name: "Mimi") }
container.register(Animal.self, name: "dog") { _ in Dog(name: "Hachi") }
The following registrations can co-exist in a container because the numbers of arguments are different. The first registration has no arguments, and the second has an argument:
container.register(Animal.self) { _ in Cat(name: "Mimi") }
container.register(Animal.self) { _, name in Cat(name: name) }
The following registrations can co-exist in a container because the types of the arguments are different. The first registration has String
and Bool
types. The second registration has Bool
and String
types in the order:
container.register(Animal.self) { _, name, running in
Horse(name: name, running: running)
}
container.register(Animal.self) { _, running, name in
Horse(name: name, running: running)
}
Be careful with the types of arguments when you resolve an instance from a container. To resolve it as you expect, the types must be inferred as the same types as those you register to the container.
// Registers with name argument as String.
// The argument is inferred as String because Cat initializer takes an argument as String.
// The Registration Key is (Animal, (String) -> Animal)
container.register(Animal.self) { _, name in Cat(name: name) }
// This is the correct Registration Key (Animal, (String) -> Animal)
let name1: String = "Mimi"
let mimi1 = container.resolve(Animal.self, argument: name1) // Returns a Cat instance.
// Cannot resolve since the container has no Registration Key matching (Animal, (NSString) -> Animal)
let name2: NSString = "Mimi"
let mimi2 = container.resolve(Animal.self, argument: name2) // Returns nil.
// Cannot resolve since the container has no Registration Key matching (Animal, (Optional<String>) -> Animal)
let name3: String? = "Mimi"
let mimi3 = container.resolve(Animal.self, argument: name3) // Returns nil.
// Cannot resolve since the container has no Registration Key matching (Animal, (ImplicitlyUnwrappedOptional<String>) -> Animal)
let name4: String! = "Mimi"
let mimi4 = container.resolve(Animal.self, argument: name4) // Returns nil.