Firebolt is a dependency injection framework written for Swift
. Inspired by Kotlin
Koin. This framework is meant to be lightweight and unopinionated by design with resolutions working simply by good old functional programming.
Firebolt
is an open-source project, feel free to contact me if you want to help contribute to this codebase. You can also do a pull-request or open up issues.
Firebolt
is available through CocoaPods. To install
it, simply add the following line to your Podfile:
pod 'Firebolt'
url: https://github.com/DrewKiino/Firebolt.git
from: 0.3.6
- Usage
- Scope
- Arguments
- Protocol Conformance
- Thread Safety
- Global Resolver
- Multiple Resolvers
- Subclassing Resolvers
- Unregister Dependencies
- Storyboard Resolution
- Examples
- Instantiate a
Resolver
let resolver = Resolver()
- Register dependencies.
class ClassA {}
resolver.register { ClassA() }
- Use the
get()
qualifier to resolve inner dependencies.
class ClassA {}
class ClassB { init(classA: ClassA) }
try resolver
.register { ClassA() }
.register { ClassB(classA: get()) } // <-- get() qualifier
- Start coding with dependency injection using the
get()
keyword.
let classA: ClassA = get()
let classB: ClassB = get()
You can pass in a scope
qualifier during registration to tell the Resolver
how you want to instance to be resolved.
The current supported forms of scope
are:
enum Scope {
case single // <- the same instance is resolved each time
case factory // <- unique instances are resolved each time
}
You can set scope like this. .single
is the default scope setting.
resolver.register(.single) { ClassA() } /// only a single instance will be created and shared when resolved
resolver.register(.factory) { ClassA() } /// multiple instances are created each time when resolved
resolver.register { ClassA() } /// .single is the default
// now these two are of the same instances
let classA: ClassA = get()
let classA: ClassA = get()
Singleton resolutions also apply to protocols of concrete classes as well.
resolver.register(.single, expect: ClassAProtocol.self) { ClassA() }
let classA1: ClassAProtocol = get()
let classA2: ClassAProtocol = get()
// Both ClassA1 and ClassA2 are resolved from the same concrete instance
You can pass in arguments during registration like so.
let resolver = Resolver()
let environment: String = "stage"
reasolver.register { ClassD(environment: environment, classA: get()) }
If the arguments need to be passed in at the call site. You can specify the expected type during registration.
resolver.register(arg1: String.self) { ClassD(environment: $0) }
Then you can pass in the argument afterwards.
let classD: ClassD = get("stage")
You can pass in multiple arguments as well.
resolver.register(arg1: String.self, arg2: Int.self) { ClassD(environment: $0, timestamp: $1) }
let classD: ClassD = get("stage", 1200)
You can also pass in optionals like so.
class ClassE { init(value: String?) {} }
let resolver = Resolver()
resolver.register(arg1: String?.self) { ClassE($0) }
// no arguments tells the resolver to pass nil instead
let classE: ClassE = get()
let classE: ClassE = get("SOME_VALUE")
For shared non-registered arguments between dependencies, you can pass in arguments from within the register
block using the upstream argument themselves.
let resolver = Resolver()
class ClassC {
init(classA: ClassA, classB: ClassB) {}
}
resolver
.register(arg1: ClassA.self) { ClassB(classA: $0) }
.register(arg1: ClassA.self) {
// ClassA is now shared between ClassB and ClassC
// without registration
ClassC(classA: $0, classB: get($0))
}
// Then call them like so
let classA: ClassA = ClassA()
let classC: ClassC = get(classA)
Protocol conformance is also supported by the Resolver
. Let's say you want to have a ClassA
protocol and a ClassAImpl
concrete type registered, you can use the expect
argument.
protocol ClassA { func foo() }
class ClassAImpl: ClassA { func foo() {} }
let resolver = Resolver()
resolver.register(expect: ClassA.self) { ClassAImpl() }
Then when calling it in the callsite.
let classA: ClassA = get() // <- ClassAImpl will be returned
You are also able to have support for multiple protocols for the same concrete type.
protocol ClassAVariantA { func foo() }
protocol ClassAVariantB { func bar() }
class ClassA: ClassAVariantA, ClassAVariantB {
func foo() {}
func bar() {}
}
let resolver = Resolver()
resolver.register { ClassA() }
// multiple resolutions using the same concrete type with the expect qualifier
let variantA: ClassAVaraintA = get(expect: ClassA.self)
let variantB: ClassAVaraintB = get(expect: ClassA.self)
Or using a different method, passing multiple expects for the same concrete class.
let resolver = Resolver()
resolver.register(expects: [ClassAVaraintA.self, ClassAVaraintB.self]) { ClassA() }
let classA: ClassAVaraintA? = get()
let classA: ClassAVaraintB? = get()
If there are dependencies that require protocol conformance but you are only supporting a concrete class you can do the following:
class ClassA: ClassAVariantA {}
class ClassB { init(classAVariant: ClassAVariantA) {} }
let resolver = Resolver()
resolver
.register { ClassA() }
.register { ClassB(classAVariant: get(expect: ClassA.self)) }
// works
let classB: ClassB = get()
This works because ClassA
is registered in the dependency scope
but we are able to cast it to the expected type ClassAVaraintA
by using the get()
qualifier and the expect
argument passed in during the callsite.
Firebolt
has a internal global queue that makes sure dependencies and resolvers are registered/unregistered in the same sequence.
Normally, if you initialze a Resolver
without a resolver identifier passed in, you will get the GlobalResolver
. You can register and unregister dependencies with this special resolver as well as having resolutions without much work for your application.
let resolver = Resolver() // <-- GlobalResolver created
resolver.register { ClassA() }
You can then globally inject dependencies without specifying a Resolver identifier.
// property scoped in another instance of the application
// will resolve automatically for you.
let classA: ClassA = get()
Although you can pass in a resolverId
if you want global resolution to a specific Resolver
regardless as the GlobalResolver
is shared across all instances naturally.
let classA: ClassA = get("Resolver_1")
If you want to keep dependencies separate you can instantiate multiple resolvers with each having their own scope.
When you initialize a Resolver
you have to pass in a resolverId
, Firebolt then registers this resolver in a cache.
- Instantiate a
Resolver
with a unique identifier.
let resolver1 = Resolver("Resolver_1")
resolver1.register { ClassA() }
let resolver2 = Resolver("Resolver_2")
resolver2.register { ClassA() }
// make sure to resolve using the Resolver itself using lamba
resolver2.register { ClassB(classA: $0.get()) }
- Then inject by referencing by their respective resolvers.
// resolves to `nil` because Resolver_1 never registered ClassB
let classB: ClassB = resolver1.get()
// resolves to ClassB
let classB: ClassB = resolver2.get()
Here is an example of using a Resolver
via an Interface
like design.
let resolver: Resolver
init(resolver: Resolver) { self.resolver = resolver }
func viewDidLoad() {
let classB: ClassB = resolver.get()
}
Objects not registered by the resolver won't be shared by other resolvers. This includes objects registered as .single
as well unless they are registered by the GlobalResolver
itself in which they become a true Singleton
.
If you initailize two resolvers of the same identifier, they both will share the same cache of dependencies.
let resolverA = Resolver("SAME_IDENTIFIER")
resolverA.register { ClassA() }
let resolverB = Resolver("SAME_IDENTIFIER")
// This will successfully resolve since ResolverB shares the same
// identifier as ResolverA - thus the same cache of dependencies.
let classA: ClassA = resolverB.get()
Resolvers are subclassable if you feel the need to create your own kind of a Resolver
ex: MyAppResolver
.
It is important that you pass in your own resolverId
through an initializer witin your subclass. If you don't, your subclass will inheritely be a GlobalResolver
since a standalone Resolver
class with no identifier will essentiually access the singleton itself.
class MyAppResolver: Resolver {
init() {
super.init("MyAppResolver")
}
}
let myResolver = MyAppResolver()
myResolver.register { ClassA() }
// this will work
let classA: ClassA = myResolver.get()
// this will also work
let classA: ClassA = get(resolverId: "MyAppResolver")
// this will fail
let classA: ClassA = get()
You can unregister dependencies like so.
resolver.register { ClassA() }
let classA: ClassA? = get() // will return ClassA
resolver.unregister(ClassA.self)
let classA: ClassA? = get() // will return nil
Firebolt
can be used to resolve storyboards as well. Given this example,
// There are multiple ways to initialize a storyboard view code but in this case
// we will use a static initializer for the sake of allowing external parameters
class ViewController {
class func initialize(userManager: UserManager): SecondViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(identifier: "ViewController") as! ViewController
}
}
// .. then register
resolver
.register { UserManager() }
.register { ViewController.initialize(userManager: get()) }
// ... when resolving it
let vc: ViewController = get()
// ... or if you're using a container based approach
let vc: ViewController = someResolver.get()
Application Architecture
// UserManager.swift
class UserManager {}
// ViewController.swift
class ViewController: UIViewController {
public init(userManager: UserManager) {}
}
// AppDelegate.swift
class AppDelegate {
let resolver = Resolver()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
resolver.register { UserManager() }
resolver.register { ViewController(userManager: get()) }
let viewController: ViewController = get()
window?.rootViewController = viewController
}
}
Andrew Aquino, andrewaquino118@gmail.com
Firebolt is available under the MIT license. See the LICENSE file for more info.