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.
This
class Context {
let networkProvider: NetworkProvider
let authProvider: AuthProvider
let localStorage: LocalStorage
}
let context = Context(...)
let VC = MessagesViewController(networkProvider: context.networkProvider, authProvider: context.authProvider, localStorage: context.localStorage)
//----------------------------------------------------------------
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
}
func checkIfLoggedIn() -> Bool {
self.authProvider.checkifLoggedIn()
}
}
becomes
class Context: RootInjector {
let networkProvider: NetworkProvider
let authProvider: AuthProvider
let localStorage: LocalStorage
}
let context = Context(...)
let VC: MessagesViewController = context.inject()
//------------------------------------------------------
protocol MessagesViewControllerInjector {
}
@Needs<MessagesViewControllerInjector>
@Injects<MessagesViewModelInjector>
class MessagesViewController: UIViewController {
init(injector: MessagesViewControllerInjectorImpl) {
self.injector = injector
self.viewModel = inject()
}
}
// ------------------------------------------------------
protocol MessagesViewModelInjector {
var networkProvider: NetworkProvider {get}
var authProvider: AuthProvider {get}
var localStorage: LocalStorage {get}
}
@Needs<MessagesViewModelInjector>
class MessagesViewModel {
func checkIfLoggedIn() -> Bool {
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,
init
of 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,
inject
functions 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.
Injector
- specification of dependencies of a classInjectable
- Class that needs its dependencies to be injected (viaInjector
in init)InjectsXXX
- Must be implemented by parent class that wants to injectXXX
injector.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.
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 toinit
create a protocol that will specify them and let it conform toInjector
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 toInjector
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 callEXTRA="--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 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 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 implementInjectable
and satisfy protocol requirements by creating fieldinjector
andinit(injector:...)
. Actual structs that can be used are created by the injection framework based on yourInjector
s definitions. For example for ourMessagesViewModel
we 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
MessagesViewModelInjector
can be used directly inMessagesViewModel
via extension that was automatically created byInjectGrail
. So in this case we can usenetworkManager
directly.class MessagesViewModel: Injectable { let injector: MessagesViewModelInjectorImpl init(injector: MessagesViewModelInjectorImpl) { self.injector = injector } func doSomeAction() { self.networkManager.callBackend() } }
-
For each
Injector
InjectGrail
also creates protocolInjects<InjectorName>
so in our case this would beInjectsMessagesViewModelInjector
. Classes that areInjectable
themselves and want to be able to inject to otherInjectables
can conform that protocol to create helper functioninject(...)
, that doesn injecting.InjectGrail
automatically resolves dependencies between current class'Injector
and targetInjector
and adds arguments to functioninject
for all that has not been found. Conforming toInjects<InjectorName>
also adds all dependencies of the target to current injectorImpl
.If we were to create
MessageRowViewModel
fromMessagesViewModel
. We would need to createMessageRowViewModelInjector
andlet 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
implementInjectsMessageRowViewModelInjector
and after next run of scriptMessagesViewModelInjectorImpl
would automatically get additional propertymessagesRepository
- because it's provided byRootInjector
, andMessagesViewModel
would be extended with functionfunc inject(messageIndex: Int) -> MessageRowViewModelInjector
, which it could use to createMessageRowViewModel
like so:class MessagesViewModel: Injectable { let injector: MessagesViewModelInjectorImpl init(injector: MessagesViewModelInjectorImpl) { self.injector = injector } func createRowViewModel() { let rowViewModel = MessageRowViewModel(inject(messageIndex: 0)) } }
Int
s andString
s 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
authenticationManager
will be always come from arguments toinject
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, lukasz.kwoska@swing.dev
InjectGrail is available under the MIT license. See the LICENSE file for more info.
- 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.