TinyDi is a multi-module dependency injection solution. It uses property wrapper syntax, to make it easy for you to declare & manage your dependencies.
- Supports Swift functions marked as
@TinyModule
&@Singleton
. - Supports Swift variables marked as
@Inject
&@Binds
. - Supports SwiftUI and UIKit
Swift Package Manager
File > Swift Packages > Add Package Dependency
Add https://github.com/MwaiBanda/TinyDi.git
branch main
- Additional links
Here is a small example.
extension DependencyRegistry {
func inject() {
TDi.inject(context: { reslover in
provideGreeter()
})
}
@Singleton
private func provideGreeter() {
Greeter(greeting: "Hello, Tiny World!")
}
}
func main() {
// MARK: Call initialisation function
DependencyRegistry.shared.inject()
// MARK: Dependency Retrival
@Inject var greeter: Greeter
print(greeter.greeting)
}
You can get the full code here.
TinyDi alllows for easy separations of cocerns, the library follows the dependency rule participularily with modules, dependencies within
a module can not depend on each other within the same module. To start, you have to extend the Dependenpce Registry
class
You can place up the extension function app file
// MARK: Dependency Insertion
// Step 1 :: Extend DependencyRegistry
extension DependencyRegistry {
// Step 2 :: Create an initialisation function
func inject() {
TDi.inject(context: { reslover in
// Step 3.b :: reference provider function
provideGreeter()
})
}
// Step 3.a :: Create a provider function for a dependency
@Singleton
private func provideGreeter() {
Greeter(greeting: "Hello, Tiny World!")
}
}
func main() {
// Step 4 :: Call initialisation function
DependencyRegistry.shared.inject()
// MARK: Dependency Retrival
@Inject var greeter: Greeter
print(greeter.greeting)
}
You can get the full code here.
TinyDi offers other ways to for you to provide your dependencies. Dependencies can be provide with
the following property-wrappers & @TinyModule
& @Binds
:
@TinyModule:
Functions prefixed with @TinyModule
allow you to build modules of dependencies within them.
Modules allow easier separation of different dependencies.
@TinyModule
func singletonModule(){
Module(
Single(Auth.auth(), named: "Auth"), // Optional naming of dependencies within a module
Single(DatabaseDriverFactory())
)
}
You can get the full code here.
In cases were you need dependencies from another module, make (resolver: TinyDi)
the function
signature. With reference to the resolver you can call resolver.resolve()
in place of the required
dependency. Additionally, when providing a dependency you can explicitly specify it's type, this is useful
for dependency inversion
@TinyModule
func controllerModule(resolver: TinyDi) {
Module(
Single<TransactionController>(TransactionControllerImpl(driverFactory: resolver.resolve())),
Single<AuthController>(AuthControllerImpl())
)
}
You can get the full code here.
After creating your modules, add them to the Dependency Registry
extension DependencyRegistry {
func inject() {
TDi.inject { resolver in
singletonModule()
controllerModule(resolver: resolver)
}
}
}
You can get the full code here.
@Binds:
Variables prefixed with the @Binds
allow you provide dependencies in them. A typical usecase would be dependency inversion
extension DependencyRegistry {
func inject() {
TDi.inject { resolver in
singletonModule()
controllerModule(resolver: resolver)
@Binds var authController: AuthController = {
AuthControllerImpl()
}()
}
}
}
extension DependencyRegistry {
func inject() {
TDi.inject { resolver in
singletonModule()
controllerModule(resolver: resolver)
@Binds var authId: String = {
Auth.auth().currentUser?.id ?? ""
}()
}
}
}
You can get the full code here.
In cases, were you want to bind dependencies of the same type. Use @Binds(named: "SomeKey")
to differentiate
one from the other.
@Binds(named: String
):
extension DependencyRegistry {
func inject() {
TDi.inject { resolver in
singletonModule()
controllerModule(resolver: resolver)
@Binds(named: "APIKey") var apiKey: String = {
"XXX.xxx.xx.00"
}()
}
}
}
You can get the full code here.
Variables prefixed with the @Inject
allow you retrieve dependencies from them by
declaring an explicity type variable
@Inject:
class TransactionViewModel: ObservableObject {
@Inject private var controller: TransactionController
...
Alternatively, you can retrive specific named dependencies
@Inject(named: String
):
class AuthViewModel: ObservableObject {
@Inject(named: "Auth") private var auth: Auth
...
TinyDi can also be used to provide your test dependencies
Step 1:
Create your test modules
import Foundation
import XCTest
@testable import TinyDi
@TinyModule
func authModule() {
Module(
Single<AuthProviding>(Auth())
)
}
@TinyModule
func dataModule(resolver: TinyDi) {
Module(
Single<DataProviding>(Data(auth: resolver.resolve()))
)
}
You can get the full code here.
Step 2:
Extend DependencyRegistry
& create an initialisation function then add your modules
import XCTest
@testable import TinyDi
import Foundation
extension DependencyRegistry {
func injectTest() {
TDi.inject { resolver in
authModule()
dataModule(resolver: resolver)
}
}
}
You can get the full code here.
Step 3:
Create a base test class
import Foundation
import XCTest
@testable import TinyDi
class BaseXCTestCase: XCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()
DependencyRegistry.shared.clear {
DependencyRegistry.shared.injectTest()
}
}
override func tearDownWithError() throws {
try super.tearDownWithError()
}
}
You can get the full code here.
Use dependencies
final class TinyDiTests: BaseXCTestCase {
@Inject private var data: DataProviding
override func setUpWithError() throws {
try super.setUpWithError()
}
override func tearDownWithError() throws {
try super.tearDownWithError()
_data.release()
}
...
You can get the full code here.