This project is fully functional, but it requires a lot of attention in several areas:
If you're willing to help then by all means chime in! We are open for PRs.
This
class MessagesViewController: UIViewController {
private let networkProvider: NetworkProvider
private let authProvider: AuthProvider
private let localStorage: LocalStorage
private let viewModel: MessagesViewModel
init(networkProvider: NetworkProvider, authProvider: AuthProvider, localStorage: LocalStorage, ...) {
self.networkProvider = networkProvider
self.authProvider = authProvider
self.localStorage = localStorage
self.viewModel = MessagesViewModel(networkProvider: networkProvider, authProvider: authProvider, localStorage: localStorage, ...)
}
}
// ------------------------------------------------------------------
class MessagesViewModel {
let networkProvider: NetworkProvider
let authProvider: AuthProvider
let localStorage: LocalStorage
init(networkProvider: NetworkProvider, authProvider: AuthProvider, localStorage: LocalStorage, ...) {
self.networkProvider = networkProvider
self.authProvider = authProvider
self.localStorage = localStorage
self.authProvider.checkifLoggedIn()
}
}
becomes
protocol MessagesViewControllerInjector: Injector {
}
class MessagesViewController: UIViewController, Injectable, InjectsMessagesViewModelInjector {
let injector: MessagesViewControllerInjectorImpl
init(injector: MessagesViewControllerInjectorImpl) {
self.injector = injector
self.viewModel = MessagesViewModel(inject())
}
}
// ------------------------------------------------------------------
protocol MessagesViewModelInjector: Injector {
var networkProvider: NetworkProvider {get}
var authProvider: AuthProvider {get}
var localStorage: LocalStorage {get}
}
class MessagesViewModel: Injectable {
let injector: MessagesViewModelInjectorImpl
init(injector: MessagesViewModelInjectorImpl) {
self.injector = injector
self.authProvider.checkifLoggedIn()
}
}
init
of each class contains only those dependencies that are trully needed by it or it's children (wrapped in a simple struct),inject
functions take as arguments dependencies that have not been found in current class, but are required by children.Injector
- specification of dependencies of a classInjectable
- Class that needs its dependencies to be injected (via Injector
in init)InjectsXXX
- Must be implemented by parent class that wants to inject XXX
injector.RootInjector
- Class or struct that implements this protocol will be automatically able to injects all Injectors
. This is a top of injection tree. There must be exactly one class implementing this protocol.InjectGrail is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'InjectGrail'
import InjectGrail
For every class that needs to be Injectable
instead o passing arguments directly to init
create a protocol that will specify them and let it conform to Injector
protocol.
For example, let's say we have a MessagesViewModel
which we want to be injectable.
class MessagesViewModel {
let networkManager: NetworkManager
init(networkManager: NetworkManager) {
self.networkManager = networkManager
}
}
We need to create MessagesViewModelInjector
- name doesn't matter. By convention we use <InjectableClassName>Injector
and we let it conform to Injector
protocol MessagesViewModelInjector: Injector {
var networkManager: NetworkManager {get}
}
Add a new build script (before compilation):
"$PODS_ROOT/InjectGrail/Scripts/inject.sh"
If you ever encounter issues with underlying sourcery magic, you can pass extra arguments to it using EXTRA
environment variable. For example to disable sourcery cache you can call EXTRA="--disableCache" "$PODS_ROOT/InjectGrail/Scripts/inject.sh"
Add a class or struct that implements RootInjector
. This will be your top most injector capable for injecting all other Injectables
.
Injectables can be created manually as well.
struct RootInjectorImpl: RootInjector {
let networkManager: NetworkManager
let messagesRepository: MessagesRepository
let authenticationManager: AuthenticationManager
}
Compile. Injecting script will generate files /InjectGrail/RootInjector.swift
, /InjectGrail/Injectors.swift
and /InjectGrail/Injectables.swift
in your project folder. Add them to project (and as an Output of buildstep added in previous steps).
For every class that needs to be Injectable
let it implement Injectable
and satisfy protocol requirements by creating field injector
and init(injector:...)
. Actual structs that can be used are created by the injection framework based on your Injector
s definitions. For example for our MessagesViewModel
we created protocol MessagesViewModelInjector
, so injection framework created implementation in struct MessagesViewModelInjectorImpl
(added Impl
). We should use that.
class MessagesViewModel: Injectable {
let injector: MessagesViewModelInjectorImpl
init(injector: MessagesViewModelInjectorImpl) {
self.injector = injector
}
}
All properties from MessagesViewModelInjector
can be used directly in MessagesViewModel
via extension that was automatically created by InjectGrail
. So in this case we can use networkManager
directly.
class MessagesViewModel: Injectable {
let injector: MessagesViewModelInjectorImpl
init(injector: MessagesViewModelInjectorImpl) {
self.injector = injector
}
func doSomeAction() {
self.networkManager.callBackend()
}
}
For each Injector
InjectGrail
also creates protocol Injects<InjectorName>
so in our case this would be InjectsMessagesViewModelInjector
. Classes that are Injectable
themselves and want to be able to inject to other Injectables
can conform that protocol to create helper function inject(...)
, that doesn injecting. InjectGrail
automatically resolves dependencies between current class' Injector
and target Injector
and adds arguments to function inject
for all that has not been found. Conforming to Injects<InjectorName>
also adds all dependencies of the target to current injector Impl
.
If we were to create MessageRowViewModel
from MessagesViewModel
. We would need to create MessageRowViewModelInjector
and let MessageRowViewModel implement Injectable
, like so:
protocol MessageRowViewModelInjector: Injector {
var messagesRepository: MessagesRepository {get}
var messageIndex: Int {get}
}
class MessageRowViewModel: Injectable {
let injector: MessageRowViewModelInjectorImpl
init(injector: MessageRowViewModelInjectorImpl) {
self.injector = injector
}
}
After running injection script we can make MessagesViewModel
implement InjectsMessageRowViewModelInjector
and after next run of script MessagesViewModelInjectorImpl
would automatically get additional property messagesRepository
- because it's provided by RootInjector
, and MessagesViewModel
would be extended with function func inject(messageIndex: Int) -> MessageRowViewModelInjector
, which it could use to create MessageRowViewModel
like so:
class MessagesViewModel: Injectable {
let injector: MessagesViewModelInjectorImpl
init(injector: MessagesViewModelInjectorImpl) {
self.injector = injector
}
func createRowViewModel() {
let rowViewModel = MessageRowViewModel(inject(messageIndex: 0))
}
}
Int
s and String
s are never resolved during injection. Even if Injecting class also has it in its Injector
.
Resolving migh be also disabled manually for field in Injector by adding Sourcery annotation:
protocol MessageRowViewModelInjector: Injector {
var messagesRepository: MessagesRepository {get}
// sourcery: forceManual
var authenticationManager: AuthenticationManager {get}
var messageIndex: Int {get}
}
In the example above authenticationManager
will be always come from arguments to inject
function of injecting classes.
When resolving dependency against parent Injector InjectGrail
searches via type definition. If there are multiple properties of the same type, then it additionally matches by name. As mentioned above Int
s and String
s are never resolved.
Łukasz Kwoska, [email protected]
InjectGrail is available under the MIT license. See the LICENSE file for more info.
link |
Stars: 0 |
Last commit: 1 week ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics