Skip to content
/ Depin Public

Dependency injection based on Swinject and Injected property wrapper

License

Notifications You must be signed in to change notification settings

KOMA-Inc/Depin

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Depin

Depin is a library for dependency injection based on Swinject but with some additional wrapping logic.

The overall approach is like this: register dependencies on app launch, then simply use them.

Register dependencies

You should register dependencies as soon as possible in your app. Here is an example:

import Depin
import UIKit

@main
enum Main {

    @Space(\.dependenciesAssembler)
    private static var assembler: Assembler

    static func main() {

        assembler.apply(assemblies: [
            // ...
        ])

        UIApplicationMain(
            CommandLine.argc,
            CommandLine.unsafeArgv,
            nil,
            NSStringFromClass(AppDelegate.self)
        )
    }
}

Here you retrieve a Swinject's Assembler from Space (it is an analog for SwiftUI's Environment). After that, you should pass an array of assemblies, each of which contains logic for dependencies' registration. For example:

import Depin

class AppAssembly: Assembly {
    
    func assemble(container: Swinject.Container) {
        
        container.register(ModuleBuilder.self) {
            ModuleBuilder()
        }

        container.registerSynchronized(ApplicationLifeCycleCompositor.self) { r in
            ApplicationLifeCycleCompositor(delegates: [
                r ~> AnalyticClientCompositor.self,
                r ~> LifecycleHandler.self
            ])
        }
    }
}

Assembler from the first example has a property Container, which is used in assemblies to register dependencies. The same Container is later used to retrieve them. You will register dependencies in either of two ways:

  1. Use this function if the dependency has no dependencies that must be passed in init, as with ModuleBuilder case
register<Service>(
        _ serviceType: Service.Type,
        name: String? = nil,
        factory: @escaping () -> Service
    )
  1. Use this function if the dependency has dependencies that must be passed in init, as with ApplicationLifeCycleCompositor case
registerSynchronized<Service>(
        _ serviceType: Service.Type,
        name: String? = nil,
        factory: @escaping (Resolver) -> Service
    )

Revtriving dependencies

To retrieve the dependency, simply use @Injected property wrapper

class FeedbackFormViewStateFactory {

    @Injected private var i18n: I18n

    // ...
}

Register as singleton

When you register a dependency, you pass a closure that should create an instance of this dependency. Meaning, that if different classes has the same dependency, you will get different instances of it. If you need a singleton behavior, register the dependency like this:

container.register(Test.self) {
    Test()
}
.inObjectScope(.container)

Register the same dependency with different names

What if you need several different singleton-like instances? You should register it with name. Here is an approach you can use to avoid using string:

  1. Create a enum with services types an conform it to ServiceName
import Depin

enum DepinServiceType: String {
    case test
}

extension DepinServiceType: ServiceName {
    var name: String {
        rawValue
    }
}
  1. Create an extension for Container in your app
import Depin

extension Container {
    @discardableResult
    func register<Service>(
        _ service: Service.Type,
        serviceType: DepinServiceType,
        factory: @escaping (Resolver) -> Service
    ) -> ServiceEntry<Service> {
        register(service, name: serviceType.name, factory: factory)
    }

    @discardableResult
    func register<Service>(
        _ service: Service.Type,
        serviceType: DepinServiceType,
        factory: @escaping () -> Service
    ) -> ServiceEntry<Service> {
        register(service, name: serviceType.name, factory: factory)
    }
}
  1. Create an extension for Injected in your app
import Depin

extension Injected {
    convenience init(_ serviceName: DepinServiceType) {
        self.init(serviceName as ServiceName)
    }
}
  1. Use a created registration method
container.register(EventEmitter<Void>.self, serviceType: .test) {
    EventEmitter<Void>()
}
.inObjectScope(.container)
  1. Retrieve a dependency this way
@Injected(.test) private var eventEmitter: EventEmitter<Void>

Unit testing

Use this approach to avoid stating your app when running unit tests

import Depin
import UIKit

@main
enum Main {

    @Space(\.dependenciesAssembler)
    private static var assembler: Assembler

    static func main() {

        assembler.apply(assemblies: [
            // ...
        ])

        var delegateClassName: String? {
            if NSClassFromString("XCTestCase") != nil { // Unit Testing
                nil
            } else { // App or UI testing
                NSStringFromClass(AppDelegate.self)
            }
        }

        UIApplicationMain(
            CommandLine.argc,
            CommandLine.unsafeArgv,
            nil,
            delegateClassName
        )
    }
}

Then, in your unit tests code, register mock dependencies for tested services

About

Dependency injection based on Swinject and Injected property wrapper

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages