Impose

3.1.4

Impose is a simple dependency injection library for Swift
hainayanda/Impose

What's New

v3.1.4

2022-06-30T08:09:26Z

Since some projects will have different Quick and Nimble versions, and it shouldn't include in the distribution, it is now removed from dependency in Swift Package Manager version.

Impose

Impose is a simple dependency injection library for Swift

codebeat badge build test SwiftPM Compatible Version License Platform

Requirements

  • Swift 5.0 or higher (or 5.3 when using Swift Package Manager)
  • iOS 9.3 or higher (or 10 when using Swift Package Manager)

Only Swift Package Manager

  • macOS 10.10 or higher
  • tvOS 10 or higher

Installation

Cocoapods

Impose is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Impose', '~> 3.1.3'

Swift Package Manager from XCode

  • Add it using XCode menu File > Swift Package > Add Package Dependency
  • Add https://github.com/hainayanda/Impose.git as Swift Package URL
  • Set rules at version, with Up to Next Major option and put 3.1.4 as its version
  • Click next and wait

Swift Package Manager from Package.swift

Add as your target dependency in Package.swift

dependencies: [
    .package(url: "https://github.com/hainayanda/Impose.git", .upToNextMajor(from: "3.1.4"))
]

Use it in your target as Impose

 .target(
    name: "MyModule",
    dependencies: ["Impose"]
)

Author

Nayanda Haberty, hainayanda@outlook.com

License

Impose is available under the MIT license. See the LICENSE file for more info.

Basic Usage

Impose is very easy to use and straightforward, all you need to do is provide some provider for dependency:

Injector.shared.addSingleton(for: Dependency.self, SomeDependency())

and then use it in some of your classes using property wrapper or using global function

class InjectedByPropertyWrapper {
    @Injected var dependency: Dependency
    
    ...
    ...
}

class InjectedByInit {
    var dependency: Dependency
    
    init(dependency: Dependency = inject(Dependency.self)) {
        self.dependency = dependency
    }
}

the provider is autoClosure type, so you can do something like this:

Injector.shared.addSingleton(for: Dependency.self) {
    dependency: SomeDependency = .init()
    dependency.doSomeSetup()
    return dependency
}

the provider automatically just creates one instance only from calling closure and reused the instance, so the closure is only called once. If you want the provider to call closure for every injection, you can use addTransient method:

Injector.shared.addTransient(for: Dependency.self, SomeDependency())

Don't forget that it will throw an uncatchable Error if the provider is not registered yet. If you want to catch the error manually, just use tryInject instead:

class InjectedByInit {
    var dependency: Dependency
    
    init(dependency: Dependency? = nil) {
        do {
            self.dependency = dependency ?? try tryInject(for: Dependency.self)
        } catch {
            self.dependency = DefaultDependency()
        }
    }
}

Safe Injection

Sometimes you just don't want your app to be throwing errors just because it's failing in dependency injection. In those cases, just use @SafelyInjected attribute or injectIfProvided function. It will return nil if the injection fails:

class InjectedByPropertyWrapper {
    @SafelyInjected var dependency: Dependency?
    
    ...
    ...
}

class InjectedByInit {
    var dependency: Dependency
    
    init(dependency: Dependency? = injectIfProvided(for: Dependency.self)) {
        self.dependency = dependency
    }
}

You can always give closure or auto closure to call if the injection fails:

class InjectedByInit {
    var dependency: Dependency
    
    init(dependency: Dependency? = inject(Dependency.self, ifFailUse: SomeDependency())) {
        self.dependency = dependency
    }
}

Singleton Provider

The simplest injection Provider is Singleton provider. The provider just creates one instance, stores it, and reused the instance, so the closure is only called once. The instance will not be released until the Injector is released. It will be useful for shared instance dependencies:

Injector.shared.addSingleton(for: Dependency.self, SomeDependency())

Transient Provider

Different from Singleton, Transient will not store the dependency at all, it will just recreate the dependency every time it's needed. The closure will be stored strongly tho. It will be useful for services that store nothing:

Injector.shared.addTransient(for: Dependency.self, SomeDependency())

Weak Provider

This provider is a combination of singleton and transient providers. It will store the instance in a weak variable before returning it. Once the stored instance became nil, it will recreate a new instance for the next injection. The closure will be stored strongly tho. It will be useful for dependency that you want to use and shared but released when not used anymore:

Injector.shared.addWeakSingleton(for: Dependency.self, SomeDependency())

Scoped Provider

You can scope your dependency so it will create a new singleton instance within a scope:

Injector.shared.addScoped(for: Dependency.self, SomeDependency())

To scope an object, you need to implement the Scopable protocol, as a mark that this object could have a different scope than global dependencies. Without a scope, this will behave like a singleton dependency:

class MyObject: Scopable {
    @Injected var dependency: Dependency
    
    ...
    ...
}

Scopable is declared like this:

public protocol Scopable {
    var scopeContext: InjectContext { get }
    func scoped(by context: InjectContext)
    func scopedUsingSameContext(as scope: Scopable)
}

you can create your context like this:

let myContext = Injector.shared.newScopedContext()

then use it for any object you need so it will then inject scoped dependency using that context instead of the global one:

myObject.scoped(by: myContext)
myOtherObject.scoped(by: myContext)
myAnyOtherObject.scopedUsingSameContext(as: myObject)

All of those three objects will have the same instance of the same scoped dependency. Any other object with no scope or different scope will have a different instance.

You can use ScopableInitiable instead if you want to have the capabilities to have to init with scope:

class MyObject: ScopableInitiable {
    @Injected var dependency: Dependency
    
    required init(using context: InjectContext) {
        scoped(by: context)
    }
    ...
    ...
}

There is one property wrapped named Scoped that can be used to make sure that property will be scoped using the same context when scoped(by:) is called or when the property is assigned:

class MyObject: Scopable {
    @Injected var dependency: Dependency
    @Scoped var myScopable: ScopableObject = .init()
    ...
    ...
}

Circular Dependency

Injected and SafelyInjected propertyWrapper will resolve dependency lazily, thus it will work even if you have a circular dependency. But it will rise stack overflow error if you use inject function instead on init since it will resolve the dependency right away. Even tho circular dependency is not recommended, it will be better if you use propertyWrapper instead for injection to avoid this problem.

Multiple Types for one Provider

You can register multiple types for one provider if you need to:

Injector.shared.addSingleton(for: [Dependency.self, OtherDependency.self], SomeDependency())

or for transient:

Injector.shared.addTransient(for: [Dependency.self, OtherDependency.self], SomeDependency())

or even for scoped:

Injector.shared.addScoped(for: [Dependency.self, OtherDependency.self], SomeDependency())

Multiple Injectors

You could have multiple Injector to provide different dependencies for the same type:

Injector.shared.addTransient(for: Dependency.self, Primary())

let secondaryInjector = Injector()
secondaryInjector.addTransient(for: Dependency.self, Secondary())

to use the other injector, switch it:

Injector.switchInjector(to: secondaryInjector)

to switch back is as easy as calling the void method:

Injector.switchToDefaultInjector()

Module Provider

If you have a modular project and want the individual module to inject everything manually by itself. You can use ModuleProvider protocol, and use it as a provider in the main module:

// this is in MyModule
class MyModuleInjector: ModuleProvider {

    func provide(for injector: Injector) {
        injector.addSingleton(for: Dependency.self, SomeDependency())
    }
}

then let's say in your AppDelegate:

import Impose
import MyModule

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(
            _ application: UIApplication,
            didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        provideDependencies()
        // do something
        return true
    }
    
    func provideDependencies() {
        Injector.shared.provide(using: MyModuleInjector())
    }
}

It will call provide(using:) with the given Injector. You can add as many ModuleProvider as you need, but if the Module provides the same Dependency for the same type of resolver, it will override the previous one with the new one.

Contribute

You know how, just clone and do pull request

Description

  • Swift Tools 5.3.0
View More Packages from this Author

Dependencies

  • None
Last updated: Tue Nov 08 2022 01:36:36 GMT-0500 (GMT-05:00)