Basic DI framework in Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public struct Service { | |
public enum LifeCycle { | |
case singleton | |
case factory | |
} | |
/// Holds the lifecycle of the current service | |
public var cycle: LifeCycle | |
/// Unique name for each service | |
public let name: ObjectIdentifier | |
/// The closure that will resolve the service | |
private let resolve: (Dependencies) -> Any | |
var instance: Any? | |
func createInstance(d: Dependencies) -> Any { | |
return resolve(d) | |
} | |
/// Initialize a service with a resolver | |
public init<Service>(_ cycle: LifeCycle = .factory, _ resolve: @escaping (Dependencies) -> Service) { | |
self.name = ObjectIdentifier(Service.self) | |
self.resolve = resolve | |
self.cycle = cycle | |
} | |
public init<Service>(of: Service.Type, _ cycle: LifeCycle = .factory, _ resolve: @escaping (Dependencies) -> Service) { | |
self.init(cycle, resolve) | |
} | |
} | |
public class Dependencies { | |
public var factories: [ObjectIdentifier: Service] = [:] | |
private init() { } | |
deinit { | |
factories.removeAll() | |
} | |
} | |
private extension Dependencies { | |
/// Resolve a serice based on its ObjectIdentifier | |
func resolve<Service>() -> Service { | |
var service = self.factories[ObjectIdentifier(Service.self)]! | |
guard let instance = service.instance, | |
service.cycle == .singleton else { | |
service.instance = service.createInstance(d: self) | |
self.factories[service.name] = service | |
return service.instance as! Service | |
} | |
return instance as! Service | |
} | |
/// Register a service with our resolver | |
private func register(_ service: Service) { | |
self.factories[service.name] = service | |
} | |
} | |
public extension Dependencies { | |
/// Create a overridable main resolver | |
static var main = Dependencies() | |
func get<Service>() -> Service { | |
return resolve() | |
} | |
@_functionBuilder struct DependencyBuilder { | |
public static func buildBlock(_ services: Service...) -> [Service] { services } | |
public static func buildBlock(_ service: Service) -> Service { service } | |
} | |
convenience init(@DependencyBuilder _ services: () -> [Service]) { | |
self.init() | |
services().forEach { register($0) } | |
} | |
convenience init(@DependencyBuilder _ service: () -> Service) { | |
self.init() | |
register(service()) | |
} | |
func build() { | |
Self.main = self | |
} | |
} | |
@propertyWrapper | |
struct Inject<Service> { | |
typealias DelayedInjection = () -> Service | |
var service: Service? | |
var delayed: DelayedInjection? | |
init() { | |
delayed = { Dependencies.main.resolve() } | |
} | |
var wrappedValue: Service { | |
mutating get { | |
if let service = service { | |
return service | |
} else if let delayed = delayed { | |
service = delayed() | |
return service! | |
} else { | |
fatalError() | |
} | |
} | |
} | |
} | |
func inject<Service>() -> Service { | |
Dependencies.main.get() | |
} | |
// Sample usage. Invoke initDI() in your app entry point (likely AppDelegate). | |
func initDI() { | |
Dependencies { | |
Service(of: AuthApi.self, .singleton) { _ in AuthApiImpl() } | |
Service { IntakeFormViewModel(repo: $0.get()) } | |
}.build() | |
} | |
// Inject dependencies either with @Inject or inject() | |
// @Inject var rootViewModel: RootViewModel | |
// RootView(viewModel: inject()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment