Injineer is simpler than other DI frameworks for Objective-C, but just as powerful. The framework introduces no dependencies in your code, so using it is almost completely transparent (see below).
To install using CocoaPods, add the following line to your Podfile:
pod 'Injineer'
Alternatively, you can just drag and drop INJContainer.h
and INJContainer.m
into your Xcode project, agreeing to copy files if needed.
I converted the example of another DI framework (Typhoon) to use Injineer, found in the Example folder. Just type pod install
in the Example directory before running it.
I didn't update the unit tests, because they rely on test utilities provided by Typhoon. You don't need such utils with Injineer. You can either create a new container for your tests, or just initialize the objects you are testing manually.
The whole project consists of a single class: INJContainer
, where you register your objects and their dependencies.
You can add two types of objects: an instance
and a provider
.
- (void) addInstance: (id) instance
forName: (NSString *) name;
Instances are just objects that have a name, they don't have dependencies. If an object depends on a name registered to an instance, it's the value you set here that will be injected.
- (void) addProviderForName: (NSString *) name
depedencies: (NSArray *) dependencies
creator: (id (^)(NSDictionary *values)) creator;
- (void) addProviderForName: (NSString *) name
depedencies: (NSArray *) dependencies
options: (INJProviderOptions) options
creator: (id (^)(NSDictionary *values)) creator;
Providers are a bit more complex than instances. They too have names, but can also have dependencies that are just names of other providers or instances. Every provider has a creator block whose job is to construct and return an object.
A provider takes a dictionary as parameter that contains the resolved value for each dependency name.
If you use constructor injection, you can pass the corresponding objects from the values dictionary to you init
method.
For each property of the created object that is nil
, which has a setter and a name from the dependencies list, the framework will try to set it to the resolved value of the corresponding dependency. You can turn off this behavior by passing the INJProviderOptionManualInit
option.
By default a provider's constructor block is called each time another provider depends on the first provider's name. You can make your provider a singleton by adding the INJProviderOptionSingleton
in options. In that case, the provider's constructor block is called once, and the returned instance is given to all other providers that depend on it.
After you specify all your instances and providers, you can call the checkForErrors
method, which ensures you are not depending on names that are not added to the container. This can usually occur due to spelling mistakes. Additionally, it does a topological sort of the dependency graph in order to find any circular dependencies, and throws an exception listing them.
Sometimes you want to have more control when an object you depend on is created. A good example of this are view controllers. Say you we have the following situation:
[container addInstance: [[ProductsService alloc] init]
forName: "productsService"];
[container addProviderForName: @"productsListViewController"
dependencies: @[ @"productsService", @"productDetailsViewController" ]
options: 0
creator: ^id(NSDictionary *values) {
return [[ProductsListViewController alloc] init];
}];
[container addProviderForName: @"productDetailsViewController"
dependencies: @[ @"productsService" ]
options: 0
creator: ^id(NSDictionary *values) {
return [[ProductDetailsViewController alloc] init];
}];
Both view controllers depend on productsService
, and ProductsListViewController
also depends on ProductDetailsViewController
, the
latter of which is opened whenever a product is selected from the list. The problem with this arrangement is that the ProductDetailsViewController
is only created once. Instead what you want is to do is create a new controller each time a product is selected. To accomplish this,
you need to change the name of the dependency from productDetailsViewController
to productDetailViewControllerProvider
. What this will do is, instead of creating a new instance for ProductDetailsViewController
and injecting it in the ProductListViewController
, will rather inject a block that returns a new ProductDetailsViewController
instance.
For example, instead of ProductsListViewController
having a property:
@property (nonatomic, strong) ProductDetailsViewController *productDetailsViewController
it will have:
@property (nonatomic, strong) ProductDetailsViewController *(^productDetailsViewControllerProvider)(void)
When you want a new ProductDetailsViewController
instance, you just use the object returned from productDetailsViewControllerProvider()
. This is the only place where the framework interferes with your code organization, as in this case you need to inject a block which returns the instance you want. However, you are just adhering to a convention, not importing other code or libraries in your view controllers.
Circular dependencies are usually a code smell and Injineer does not support them out of the box, but you can still have them if you want. Suppose you have the following dependencies:
[container addProviderForName: @"rootViewController"
dependencies: @[ @"productsListViewController" ]
options: INJProviderOptionSingleton
creator: ^id(NSDictionary *values) creator {
return [[RootViewController alloc] init];
}];
[container addProviderForName: @"productsListViewController"
dependencies: @[ @"productsService", @"productDetailsViewControllerProvider" ]
options: 0
creator: ^id(NSDictionary *values) {
return [[ProductsListViewController alloc] init];
}];
[container addProviderForName: @"productsService"
dependencies: @[ @"rootViewController" ]
options: INJProviderOptionSingleton
creator: ^id(NSDictionary *values) {
return [[ProductsService alloc] init];
}];
This dependency graph contains a circular dependency of the form rootViewController
-> productsListViewController
-> productsService
-> rootViewController
. In order to untagle this situation, you can change the productService
to depend on rootViewControllerProvider
, thus breaking the cycle. When you need an instance of rootViewController
in the service, just use the one returned from rootViewControllerProvider()
.