Skip to content

Introduction to DIve

DIve is a minimalistic Swift dependency injection framework.

Visit GitHub →

High level architecture

Architecture

Pool is an immutable storage whose elements are key-value pairs, the central part of the framework that is used to pass dependencies across objects. Pools can be filled out with dependencies manually or by using loaders.

Manual dependency loading

In the example below, we define a class RandomNumber that depends on a random number generator. @Injected property wrapper is used to access this dependency.

appending function is used to add a SystemRandomNumberGenerator instance, that conforms to RandomNumberGenerator protocol, to an empty pool.

The dependency injection is performed by passing the pool to the object’s initializer.

swift
import DIve

class RandomNumber: Injectee<Pool> {
    @Injected private var generator: RandomNumberGenerator

    func generate(in range: Range<UInt64>) -> UInt64 {
        .random(in: range, using: &generator)
    }
}

let pool = Pool()
    .appending(SystemRandomNumberGenerator())

let randomNumber = RandomNumber(pool: pool)
print(randomNumber.generate(in: 0..<10))

Automatic dependency loading

The pool that we manually created in the previous example can be automatically created by loaders: SerialLoader and ConcurrentLoader objects.

swift
let loader = SerialLoader(dependencies: [
    .dependency(id: "RandomNumberGenerator", dependencies: []) { pool in
        pool.appending(SystemRandomNumberGenerator())
    }
])

let pool = try loader.load(Pool())
swift
let loader = ConcurrentLoader(dependencies: [
    .dependency(id: "RandomNumberGenerator", dependencies: []) { pool in
        pool.appending(SystemRandomNumberGenerator())
    }
])

let pool = try await loader.load(Pool())

Dependency graph

Loaders can automatically fill out pools with objects by resolving and loading dependency graphs. A dependency graph is a directed graph that represents relationships between entities, where nodes represent entities and edges represent dependencies between them. For instance, in this graph below, GreetingProviding entity depends on StringsProviding and RandomNumberGenerator.

Graph

Entities are Swift objects: protocols, classes, structures, etc.

swift
protocol GreetingProviding {
    func greeting() -> String
}

class GreetingProvider: GreetingProviding {
    /* the protocol implementation */
}

protocol StringsProviding {
    var count: Int { get }
    func string(at index: Int) -> String
}

struct StringsProvider: StringsProviding {
    /* the protocol implementation */
}

GreetingProvider generates a random index using RandomNumberGenerator object and returns a greeting string by this index from strings provided by StringsProvider. Please see the latest Swift documentation for the details about RandomNumberGenerator and SystemRandomNumberGenerator.

Graph representation

The graph can be represented in the code as a set of Dependency objects. Each dependency has a unique identifier and a list of its dependencies. Closures inside dependency objects append actual instances to the pool.

swift
extension Set where Element == AsyncInjectingDependency<Pool> {
    static let dependencies: Self = [
        .dependency(id: .randomNumberGenerator, dependencies: []) { pool in
            pool.appending(SystemRandomNumberGenerator())
        },
        .dependency(id: .stringsProviding, dependencies: []) { pool in
            pool.appending(StringsProvider(strings: [
                /* <an array of greetings> */ 
            ]))
        },
        .dependency( id: .greetingProviding, dependencies: [
            .randomNumberGenerator, .stringsProviding
        ]) { pool in
            pool.appending(GreetingProviderFactory(pool: pool).make())
        }
    ]
}

Unique identifiers

The unique identifiers are defined as an extension of DependencyId object.

swift
extension DependencyId {
    static let randomNumberGenerator: Self = "RandomNumberGenerator"
    static let stringsProviding: Self = "StringsProviding"
    static let greetingProviding: Self = "GreetingProviding"
}

Using factories

To simplify complex objects creation and in the sake of separation of concerns, factories can be used inside the closures. For instance, GreetingProviderFactory is used to create GreetingProvider.

swift
private class GreetingProviderFactory: Injectee<Pool> {
    @Injected private var generator: RandomNumberGenerator
    @Injected private var stringsProvider: StringsProviding

    func make() -> GreetingProviding {
        GreetingProvider(
            generator: generator,
            stringsProvider: stringsProvider
        )
    }
}

Loading progress handler

Loading complex dependency graphs takes time. A loading event handler can be passed to the concurrent loader to track the loading progress and update the loading user interface accordingly.

swift
actor LoadingEventHandler: ConcurrentLoadingEventHandling {
    func willLoadDependency(_ dependencyId: DependencyId) async {
        /* update UI, save data for analytics, etc. */
    }

    func didLoadDependency(_ dependencyId: DependencyId) async {
        /* update UI, save data for analytics, etc. */
    }
}

Loading dependency graph

Using the ConcurrentLoader along with the LoadingEventHandler to load the previously defined dependency graph.

swift
let loader = ConcurrentLoader(dependencies: .dependencies)
let handler = LoadingEventHandler()
let pool = try await loader.load(Pool(), handler)

Fatal errors handling

Accessing an object that is not presented in a pool leads to a fatal error. The default fatal error handler calls fatalError(_:file:line:) function that immediately terminates the app. It's possible to use a custom fatal errors handler to do extra action before the app termination.

swift
struct FatalErrorHandler: FatalErrorHandling {
    func fatalErrorHandler(_ error: Error) -> Never {
        print("A fatal error occurred: \(error)")
        fatalError()
    }
}

let pool = Pool()
    .appending(FatalErrorHandler())

Updated at:

Alex Balobanov