Shank

A Swift micro-library that provides lightweight dependency injection.

62
11
Swift

Shank

A Swift micro-library that provides lightweight dependency injection.

Read more here: https://basememara.com/swift-dependency-injection-via-property-wrapper/

Inject dependencies via property wrappers:

class ViewController: UIViewController {
    
    @Inject private var widgetModule: WidgetModuleType
    @Inject private var sampleModule: SampleModuleType
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        widgetModule.test()
        sampleModule.test()
    }
}

Register modules early in your app:

class AppDelegate: UIResponder, UIApplicationDelegate {

    private let dependencies = Dependencies {
        Module { WidgetModule() as WidgetModuleType }
        Module { SampleModule() as SampleModuleType }
    }
    
    override init() {
        super.init()
        dependencies.build()
    }
}

If you forget to build the dependency container, it will result in a run-time exception.
Since there is no type-safety to guard against this, it is recommended to
limit the dependency container to hold “modules” only:

struct WidgetModule: WidgetModuleType {
    
    func component() -> WidgetWorkerType {
        WidgetWorker(
            store: component(),
            remote: component()
        )
    }
    
    func component() -> WidgetRemote {
        WidgetNetworkRemote(httpService: component())
    }
    
    func component() -> WidgetStore {
        WidgetRealmStore()
    }
    
    func component() -> HTTPServiceType {
        HTTPService()
    }
    
    func test() -> String {
        "WidgetModule.test()"
    }
}

struct SampleModule: SampleModuleType {
    
    func component() -> SomeObjectType {
        SomeObject()
    }
    
    func component() -> AnotherObjectType {
        AnotherObject(someObject: component())
    }
    
    func component() -> ViewModelObjectType {
        SomeViewModel(
            someObject: component(),
            anotherObject: component()
        )
    }
    
    func component() -> ViewControllerObjectType {
        SomeViewController()
    }
    
    func test() -> String {
        "SampleModule.test()"
    }
}

// MARK: API

protocol WidgetModuleType {
    func component() -> WidgetWorkerType
    func component() -> WidgetRemote
    func component() -> WidgetStore
    func component() -> HTTPServiceType
    func test() -> String
}

protocol SampleModuleType {
    func component() -> SomeObjectType
    func component() -> AnotherObjectType
    func component() -> ViewModelObjectType
    func component() -> ViewControllerObjectType
    func test() -> String
}

Then resolve individual components lazily:

class ViewController: UIViewController {
    
    @Inject private var widgetModule: WidgetModuleType
    @Inject private var sampleModule: SampleModuleType
    
    private lazy var widgetWorker: WidgetWorkerType = widgetModule.component()
    private lazy var someObject: SomeObjectType = sampleModule.component()
    private lazy var anotherObject: AnotherObjectType = sampleModule.component()
    private lazy var viewModelObject: ViewModelObjectType = sampleModule.component()
    private lazy var viewControllerObject: ViewControllerObjectType = sampleModule.component()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        widgetModule.test() //"WidgetModule.test()"
        sampleModule.test() //"SampleModule.test()"
        widgetWorker.fetch(id: 3) //"|MediaRealmStore.3||MediaNetworkRemote.3|"
        someObject.testAbc() //"SomeObject.testAbc"
        anotherObject.testXyz() //"AnotherObject.testXyz|SomeObject.testAbc"
        viewModelObject.testLmn() //"SomeViewModel.testLmn|SomeObject.testAbc"
        viewModelObject.testLmnNested() //"SomeViewModel.testLmnNested|AnotherObject.testXyz|SomeObject.testAbc"
        viewControllerObject.testRst() //"SomeViewController.testRst|SomeObject.testAbc"
        viewControllerObject.testRstNested() //"SomeViewController.testRstNested|AnotherObject.testXyz|SomeObject.testAbc"
    }
}

// MARK: - Subtypes

extension DependencyTests {

struct WidgetModule: WidgetModuleType {
    
    func component() -> WidgetWorkerType {
        WidgetWorker(
            store: component(),
            remote: component()
        )
    }
    
    func component() -> WidgetRemote {
        WidgetNetworkRemote(httpService: component())
    }
    
    func component() -> WidgetStore {
        WidgetRealmStore()
    }
    
    func component() -> HTTPServiceType {
        HTTPService()
    }
    
    func test() -> String {
        "WidgetModule.test()"
    }
}

struct SampleModule: SampleModuleType {
    
    func component() -> SomeObjectType {
        SomeObject()
    }
    
    func component() -> AnotherObjectType {
        AnotherObject(someObject: component())
    }
    
    func component() -> ViewModelObjectType {
        SomeViewModel(
            someObject: component(),
            anotherObject: component()
        )
    }
    
    func component() -> ViewControllerObjectType {
        SomeViewController()
    }
    
    func test() -> String {
        "SampleModule.test()"
    }
}

struct SomeObject: SomeObjectType {
    func testAbc() -> String {
        "SomeObject.testAbc"
    }
}

struct AnotherObject: AnotherObjectType {
    private let someObject: SomeObjectType
    
    init(someObject: SomeObjectType) {
        self.someObject = someObject
    }
    
    func testXyz() -> String {
        "AnotherObject.testXyz|" + someObject.testAbc()
    }
}

struct SomeViewModel: ViewModelObjectType {
    private let someObject: SomeObjectType
    private let anotherObject: AnotherObjectType
    
    init(someObject: SomeObjectType, anotherObject: AnotherObjectType) {
        self.someObject = someObject
        self.anotherObject = anotherObject
    }
    
    func testLmn() -> String {
        "SomeViewModel.testLmn|" + someObject.testAbc()
    }
    
    func testLmnNested() -> String {
        "SomeViewModel.testLmnNested|" + anotherObject.testXyz()
    }
}

class SomeViewController: ViewControllerObjectType {
    @Inject private var module: SampleModuleType
    
    private lazy var someObject: SomeObjectType = module.component()
    private lazy var anotherObject: AnotherObjectType = module.component()
    
    func testRst() -> String {
        "SomeViewController.testRst|" + someObject.testAbc()
    }
    
    func testRstNested() -> String {
        "SomeViewController.testRstNested|" + anotherObject.testXyz()
    }
}

struct WidgetWorker: WidgetWorkerType {
    private let store: WidgetStore
    private let remote: WidgetRemote
    
    init(store: WidgetStore, remote: WidgetRemote) {
        self.store = store
        self.remote = remote
    }
    
    func fetch(id: Int) -> String {
        store.fetch(id: id)
            + remote.fetch(id: id)
    }
}

struct WidgetNetworkRemote: WidgetRemote {
    private let httpService: HTTPServiceType
    
    init(httpService: HTTPServiceType) {
        self.httpService = httpService
    }
    
    func fetch(id: Int) -> String {
        "|MediaNetworkRemote.\(id)|"
    }
}

struct WidgetRealmStore: WidgetStore {
    
    func fetch(id: Int) -> String {
        "|MediaRealmStore.\(id)|"
    }
    
    func createOrUpdate(_ request: String) -> String {
        "MediaRealmStore.createOrUpdate\(request)"
    }
}

struct HTTPService: HTTPServiceType {
    
    func get(url: String) -> String {
        "HTTPService.get"
    }
    
    func post(url: String) -> String {
        "HTTPService.post"
    }
}

// MARK: API

protocol WidgetModuleType {
    func component() -> WidgetWorkerType
    func component() -> WidgetRemote
    func component() -> WidgetStore
    func component() -> HTTPServiceType
    func test() -> String
}

protocol SampleModuleType {
    func component() -> SomeObjectType
    func component() -> AnotherObjectType
    func component() -> ViewModelObjectType
    func component() -> ViewControllerObjectType
    func test() -> String
}

protocol SomeObjectType {
    func testAbc() -> String
}

protocol AnotherObjectType {
    func testXyz() -> String
}

protocol ViewModelObjectType {
    func testLmn() -> String
    func testLmnNested() -> String
}

protocol ViewControllerObjectType {
    func testRst() -> String
    func testRstNested() -> String
}

protocol WidgetStore {
    func fetch(id: Int) -> String
    func createOrUpdate(_ request: String) -> String
}

protocol WidgetRemote {
    func fetch(id: Int) -> String
}

protocol WidgetWorkerType {
    func fetch(id: Int) -> String
}

protocol HTTPServiceType {
    func get(url: String) -> String
    func post(url: String) -> String
}

This way, only your “modules” are not type-safe, which is acceptable since
an exception with a missing module should happen early on and hopefully
obvious enough in development.

However, the individual components are type-safe and have greater flexiblity to include
parameters while resolving the component. The components should have their dependencies
injected through the constructor, which is the best form of dependency injection.
The modules get the property wrappers support and can even inject modules within modules.