InjectGrail
This project is fully functional, but it requires a lot of attention in several areas:
- Documentation,
- Example,
- Tests,
- Other process related stuff,
- Comments and other readability improvements in generated code,
- Readability improvements in Sourcery Template,
- Basic framework info. Why, Inspirations, etc...
If you're willing to help then by all means chime in! We are open for PRs.
TL;DR
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()
}
}- For each class you declare only dependencies needed by it. Not it's children.
- You don't get big bag of dependencies that you have to carry to all classes in your project.
- Dependencies are automatically pushed through hierarchy without touching parent classes definitions,
initof each class contains only those dependencies that are trully needed by it or it's children (wrapped in a simple struct),- Your classes can still be constructed manually,
injectfunctions take as arguments dependencies that have not been found in current class, but are required by children.- Not a single line of magic. You can Cmd+Click to see exact definitions. To achieve DI only protocols, structs and extensions are used.
- Command Completion for everything.
Summary of terms:
Injector- specification of dependencies of a classInjectable- Class that needs its dependencies to be injected (viaInjectorin init)InjectsXXX- Must be implemented by parent class that wants to injectXXXinjector.RootInjector- Class or struct that implements this protocol will be automatically able to injects allInjectors. This is a top of injection tree. There must be exactly one class implementing this protocol.
Requirements
Installation
InjectGrail is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'InjectGrail'Usage
-
import InjectGrail -
For every class that needs to be
Injectableinstead o passing arguments directly toinitcreate a protocol that will specify them and let it conform toInjectorprotocol.For example, let's say we have a
MessagesViewModelwhich 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>Injectorand we let it conform toInjectorprotocol MessagesViewModelInjector: Injector { var networkManager: NetworkManager {get} }
-
Add a new build script (before compilation):
"$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 otherInjectables. Injectables can be created manually as well.struct RootInjectorImpl: RootInjector { let networkManager: NetworkManager let messagesRepository: MessagesRepository let authenticationManager: AuthenticationManager }
-
Compile. Injecting script will generate file
/Generated/Inject.generated.swiftin your project folder. Add it to project. -
For every class that needs to be
Injectablelet it implementInjectableand satisfy protocol requirements by creating fieldinjectorandinit(injector:...). Actual structs that can be used are created by the injection framework based on yourInjectors definitions. For example for ourMessagesViewModelwe created protocolMessagesViewModelInjector, so injection framework created implementation in structMessagesViewModelInjectorImpl(addedImpl). We should use that.class MessagesViewModel: Injectable { let injector: MessagesViewModelInjectorImpl init(injector: MessagesViewModelInjectorImpl) { self.injector = injector } }
All properties from
MessagesViewModelInjectorcan be used directly inMessagesViewModelvia extension that was automatically created byInjectGrail. So in this case we can usenetworkManagerdirectly.class MessagesViewModel: Injectable { let injector: MessagesViewModelInjectorImpl init(injector: MessagesViewModelInjectorImpl) { self.injector = injector } func doSomeAction() { self.networkManager.callBackend() } }
-
For each
InjectorInjectGrailalso creates protocolInjects<InjectorName>so in our case this would beInjectsMessagesViewModelInjector. Classes that areInjectablethemselves and want to be able to inject to otherInjectablescan conform that protocol to create helper functioninject(...), that doesn injecting.InjectGrailautomatically resolves dependencies between current class'Injectorand targetInjectorand adds arguments to functioninjectfor all that has not been found. Conforming toInjects<InjectorName>also adds all dependencies of the target to current injectorImpl.If we were to create
MessageRowViewModelfromMessagesViewModel. We would need to createMessageRowViewModelInjectorandlet 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
MessagesViewModelimplementInjectsMessageRowViewModelInjectorand after next run of scriptMessagesViewModelInjectorImplwould automatically get additional propertymessagesRepository- because it's provided byRootInjector, andMessagesViewModelwould be extended with functionfunc inject(messageIndex: Int) -> MessageRowViewModelInjector, which it could use to createMessageRowViewModellike so:class MessagesViewModel: Injectable { let injector: MessagesViewModelInjectorImpl init(injector: MessagesViewModelInjectorImpl) { self.injector = injector } func createRowViewModel() { let rowViewModel = MessageRowViewModel(inject(messageIndex: 0)) } }
Ints andStrings are never resolved during injection. Even if Injecting class also has it in itsInjector. 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
authenticationManagerwill be always come from arguments toinjectfunction of injecting classes.
Resolving logic
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 Ints and Strings are never resolved.
Author
Łukasz Kwoska, [email protected]
License
InjectGrail is available under the MIT license. See the LICENSE file for more info.
Acknowledgement
- This project couldn't exist without Sourcery. It's the main component behind the scences.
- Annotation Inject - Thanks for showing me how easy it is to use sourcery from pod.