Introduction
In a dependency injection framework, lifetime management dictates how long an instance of a dependency should live after being created. Previously, we implemented transient lifetimes where each resolution creates a new instance. Here, we’ll introduce singleton and weak lifetimes.
Singleton
With a singleton lifetime a single instance is created and shared across the entire application.
Ideal for shared resources or objects with state that need to be accessible throughout the app and must maintain consistency.
Registration
We’ll start by defining an enum for lifetime cases.
public enum Lifetime {
case transient
case singleton
}
Since each service now has a lifetime, we encapsulate this in a service object to store both the factory and the lifetime.
final class Service<T> {
let lifetime: Lifetime
let factory: (Container) -> T
init(
_ lifetime: Lifetime,
_ factory: @escaping (Container) -> T
) {
self.lifetime = lifetime
self.factory = factory
}
}
To allow specifying a lifetime, we modify our register method. By default, dependencies will use the transient lifetime.
public func register<T>(
_ type: T.Type,
lifetime: Lifetime = .transient,
_ factory: @escaping (Container) -> T
) {
let key = String(describing: type)
services[key] = Service(lifetime, factory)
}
Resolution
To support singleton instances, we add a dictionary to cache them. When resolving a singleton, we check the cache, if the instance isn’t there, we create it, store it, and return it.
public final class Container {
private var services: [String: Any] = [:]
private var singletonServices: [String: Any] = [:]
...
public func resolve<T>(
_ type: T.Type
) -> T {
let key = String(describing: type)
guard
let service = services[key] as? Service<T>
else {
fatalError("[DI] Service for type \(type) not found!")
}
switch service.lifetime {
case .transient:
return service.factory(self)
case .singleton:
if let instance = singletonServices[key] {
return instance as! T
}
else {
let instance = service.factory(self)
singletonServices[key] = instance
return instance
}
}
}
}
Weak
A weak lifetime creates a dependency only once, then holds it weakly. If no other objects retain it, it’s deallocated. This approach is useful for dependencies that should persist only if actively used.
We first add the lifetime case for weak dependencies:
public enum Lifetime {
case transient
case singleton
case weak
}
To attain a weakly held value a common approach is to wrap instances in a weak container, like this:
final class WeakBox<T: AnyObject> {
weak var value: T?
init(value: T) {
self.value = value
}
}
While WeakBox
allows the value to become nil when unreferenced, storing it in a dictionary retains the WeakBox
itself, requiring custom clean-up like traversing the dictionary every resolution looking for nil values and removing them.
Instead, Foundation’s NSMapTable can weakly store values and it automatically clears out references when objects are no longer in use.
Note: NSMapTable
doesn’t guarantee immediate deallocation, only eventual release, likely for performance reasons. If immediate cleanup is critical, consider implementing a custom dictionary that uses WeakBox with periodic clean-up of nil values.
Create an NSMapTable
to weakly store dependencies:
private let weakServices: NSMapTable<NSString, AnyObject> = .strongToWeakObjects()
Update resolution to check weakServices for weakly-held instances:
public func resolve<T>(
_ type: T.Type
) -> T {
let key = String(describing: type)
guard
let service = services[key] as? Service<T>
else {
fatalError("[DI] Service for type \(type) not found!")
}
switch service.lifetime {
case .transient:
return service.factory(self)
case .singleton:
if let instance = singletonServices[key] {
return instance as! T
}
else {
let instance = service.factory(self)
singletonServices[key] = instance
return instance
}
case .weak:
let key = NSString(string: key)
if let instance = weakServices.object(forKey: key) {
return instance as! T
}
else {
let instance = service.factory(self) as AnyObject
weakServices.setObject(instance, forKey: key)
return instance as! T
}
}
}
Summary
This post introduced two more dependency lifetimes: singleton and weak. Future posts will cover how to create a utility method for automatically registering dependencies using their initializers as well as how to provide arguments only available at runtime when resolving dependencies.