DI Container libraries for iOS App(Swinject & NeedleFoundation)

Ryuichi / Rick
7 min readFeb 5, 2023

--

Photo by Jan Folwarczny on Unsplash

In this article, I’ll compare these awesome libraries of DI Container I had tried to introduce into my team.

⚠️ The info about these libraries is as of 02/2023.

☕️ DI Container

If you want to know what is DI Container, please check it out before continuing reading! Thank you!

🏎 Swinject

Container class is used as DI Container.

let container = Container()
・・・

🚖 Needle

A class inheriting from Component class is used as DI Container.
※ BootstrapComponent is a root of Container Hierarchy(I’ll describe later).

final class RootComponent: BootstrapComponent {
・・・
}
let rootComponent = rootComponent()
・・・

☕️ Register & Resolve

I define the following terms:

  • Register: To register/set an instance into DI Container.
  • Resolve: To instantiate/extract an instance in a registered way from DI Container if possible.

🏎 Swinject

The documentation is here.

let container = Container()
// An instance is registered with the register method of the container
container.register(Animal.self) { _ in Dog(name: "Henry") }
・・・
// We can resolve an instance from the container by calling the resolve method.
// The type we can get through a resolve method is an optional.
let animal: Animal = container.resolve(Animal.self)!

🚖 Needle

final class RootComponent: BootstrapComponent {

・・・

// The registration has done if we define a property(technically, it can be a function)
var animal: Animal {
Dog(name: "Henry")
}
}

let rootComponent = RootComponent()
// We can resolve an instance from the container just by calling it.
let animal: Animal = rootComponent.animal

☕️ Container Hierarchy

A container hierarchy is a tree of containers for the purposes of sharing the registrations of dependency injections.

From: https://github.com/Swinject/Swinject/blob/master/Documentation/ContainerHierarchy.md#container-hierarchy

🏎 Swinject

The documentation is here.

We can create a new child container using the parent container.
If the hierarchy hasn't been created as intended, we cannot resolve the dependencies.

let parentContainer = Container()
let childContainer = Container(parent: parentContainer)

🚖 Needle

Defining a container hierarchy takes these steps.

  1. Define a component class inheriting from BootstrapComponent.
    ※ The documentation is here.
  2. Define the child component class inheriting Component class and define it as a property of the parent component(which was created at 1st step).
    ※ The documentation is here.
  3. Same as 2nd step(then, the parent is a one created at 2nd).
final class RootComponent: BootstrapComponent {

・・・

var childComponent: ChildComponent {
ChildComponent(parent: self)
}
}

final class ChildComponent: Component<EmptyDependency> {
var animal: Animal {
Dog(name: "Henry")
}
}

let rootComponent = RootComponent()
let childAnimal = rootComponent.childComponent.animal

※ We set a generic parameter when inheriting Component class to define the dependencies.(I’ll describe later)

☕️ Dependencies

How do we get the dependencies to call an initializer?

🏎 Swinject

We can resolve dependencies by just using a resolve method.

// From: https://github.com/Swinject/Swinject#basic-usage

container.register(Animal.self) { _ in Dog(name: "Henry") }
container.register(Person.self) { r in
PetOwner(name: "Rick", pet: r.resolve(Animal.self)!)
}

🚖 Needle

We can set dependencies as a generic parameter of Component class. And then, we can use those dependencies through the dependency property of Component class.

  1. Define a type confirming to Dependency protocol.
  2. Define the dependencies at the type we just defined at 1st step.
  3. Specify the type we just defined at 1st step as a generic parameter of Component class.
    ※ If we don’t have to depend anything, we can use EmptyDependency instead.
  4. Use dependencies through the dependency property of component class.
// ① Define a dependency type.
proctocol PetOwnerDependency: Dependency {
// ② Define the dependencies.
var pet: Animal { get }
}

// ③ Specify the dependency as a generic parameter.
final class PetOwnerComponent: Component<PetOwnerDependency> {
var petOwner: PetOwner {
// ④ Use the dependency through the dependency property.
PetOwner(name: "Rick", pet: dependency.pet)
}
}

☕️ Management of each instance’s lifecycle

We can manage each instance’s lifecycle.

  1. Alive while it’s been referenced somewhere. In this situation, as long as the instance is referenced somewhere, it can be used and shared by others. And if no one has a reference to this instance, it’s gonna be release.
  2. Alive while a session of the app has been alive.

🏎 Swinject

// ①: Use ObjectScope.weak.
container.register(Animal.self) { _ in Dog(name: "Henry") }
.inObjectScope(.weak)

// ②: Use ObjectScope.container
container.register(Animal.self) { _ in Dog(name: "Henry") }
.inObjectScope(.container)

🚖 Needle

class AnimalComponent: Component<EmptyDependency> {

// ①: Not supported 🙅‍♀️
// That's why I created a pull request.
// https://github.com/uber/needle/pull/452

// ②: If we wrap the instance with a shared function, it will be alived while a session of the app has been alive.
var sharedAnimal: Animal {
shared { Dog(name: "Henry") }
}
}

☕️ The patterns of resolving a instance

We can specify a way of resolving a instance like below.

  1. To instantiate a new object.
  2. To extract an instance which already exists. Otherwise, instantiate a new object.
  3. To extract an instance which already exists if specified identity is same. Otherwise, instantiate a new object.
  4. To extract an instance which already exists which is shared within a custom scope. Otherwise, instantiate a new object.

🏎 Swinject

// ①: Use ObjectScope.Transient
container.register(Animal.self) { _ in Dog(name: "Henry") }
.inObjectScope(.transient)
let animal = container.resolve(Animal.self)!

// ②: Use ObjectScope.container
container.register(Animal.self) { _ in Dog(name: "Henry") }
.inObjectScope(.container)
let animal = container.resolve(Animal.self)!

// ③: If Registration Keys is same, it would be the same instance.
// I'd explained about it later.


// ④: Use Custom Scopes
class FakeStorage: InstanceStorage {
var instance: Any?
}

let customScope = ObjectScope(storageFactory: FakeStorage.init)
container.register(Animal.self) { _ in Dog(name: "Henry") }
.inObjectScope(customScope)
let animal = container.resolve(Animal.self)!

🚖 Needle

class AnimalComponent: Component<EmptyDependency> {

// ①: Just define a function.
var animal: Animal {
Dog(name: "Henry")
}

// ②: Define an instance inside of a shared function.
var sharedAnimal: Animal {
shared { Dog(name: "Henry") }
}

// ③: Define instances separately
var animal1: Animal {
Dog(name: "Henry")
}

var animal2: Animal {
Dog(name: "Henry")
}

// ④: We can achive this by separating the components.
}

☕️ Arguments

There are some situation we want to give some arguments when resolving.

🏎 Swinject

The documentation is here.

// The defenition of a way of getting an instance with name argument.
container.register(Animal.self) { _, name in Dog(name: name) }

// We can get an instance with a dog's name
let name: String = "Henry"
let animal = container.resolve(Animal.self, argument: name)

🚖 Needle

We can use a function to pass arguments to an instance.

class AnimalComponent: Component<EmptyDependency> {
// The defenition of a way of getting an instance with name argument.
func getDog(name: String) -> Animal { Dog(name: name) }
}

// We can get an instance with a dog's name
let name: String = "Henry"
let animal = animalComponent.getDog(name: name)

☕️ Registration Keys

This keys are used for extracting an existed instance.

🏎 Swinject

The documentation is here.

The key consists of:
・ The type of the service
・The name of the registration
・The number and types of the arguments

// ①: Specified type `Animal.self`.
// ②: Specified name `KeyName`.
// ③: Horse's argument `String and Bool types.`

container.register(Animal.self, name: "KeyName") { _, name, running in
Horse(name: name, running: running)
}

🚖 Needle

It works using #function like below.

// From:  https://github.com/uber/needle/blob/d3eaf696ad2e4c3e25618cfa3643402fdd237f7a/Sources/NeedleFoundation/Component.swift#L124-L154

public final func shared<T>(__function: String = #function, _ factory: () -> T) -> T {
// Use function name as the key, since this is unique per component
// class. At the same time, this is also 150 times faster than
// interpolating the type to convert to string, `"\(T.self)"`.

・・・

if let instance = (sharedInstances[__function] as? T?) ?? nil {
return instance
}
let instance = factory()
sharedInstances[__function] = instance

return instance
}
class AnimalComponent: Component<EmptyDependency> {

// The registration Key is `dog`
var dog: Animal {
shared { Dog(name: "Henry") }
}

// The registration Key is `getDog(name:)`
func getDog(name: String) -> Animal { shared { Dog(name: name) }}
}

☕️ Compile-time safety

We want to guarantee Container Hierarchy in advance and instantiate/extract an unwrapped instance.

🏎 Swinject

Not supported 🙅‍♀️
We have to unwrap the instance because a resolve function returns an optional.

let animal: Animal = container.resolve(Animal.self)!

However, we can instantiate/extract a wrapped instance by wrapping the resolve method of Swinject 🎉.

class DIContainer {
private let container: Swinject.Container

private func resolve<T>(_ :T.type, factory: (DIContainer) -> T) -> T {
guard let instance = container.resolve(T.self) else {
let newInstance = factory(self)
container.register(T.self) { _ in newInstance }
return newInstance
}
return instance
}

var owner: PetOwner {
resolve(PetOwner.self) { container in
PetOwner(name: "Rick", pet: container.pet)
}
}

var pet: Animal {
resolve(Animal.self) { _ in
Animal(name: "Henry")
}
}
}

let diContainer = DIContainer()
let owner: PetOwner = diContainer.owner

Just as a FYI, when we try to resolve a type we registered, it can cause a runtime crush in a certain condition
The documentation is here.

Unfortunatelly, there is no way in Swift (yet) to ensure that original registration conforms to the forwarded types. If you define forwarding to an unrelated type there will be a runtime crash during its resolution:

container.register(Dog.self) { _ in Dog() }
.implements(Cat.self)

let dog = container.resolve(Dog.self)! // all good
let cat = container.resolve(Cat.self)! // Precondition failed: Cannot forward Cat to Dog

🚖 Needle

A compile-time safety is supported ensuring Container Hierarchy🎉. This is ensured by Needle code generator. Thanks to this generator we don’t need to do a force unwrapping!!

The documentation is here. And overview is like below.

  1. Execute generate command (like needle generate xxx) at a compile-time or before it.
  2. And then, one file will be generated and examined whether Container Hierarchy has no contradictions. Otherwise, an error occurs.

⚠️ Needle generator can be integrated only through Carthage or Homebrew. That's why, I crated a pull request to support an installation using Pod .(https://github.com/uber/needle/pull/450)

FYI: https://github.com/uber/needle/issues/332

Thanks for reading.
Hope this article will help you ✌️

--

--