In the previous parts of the cycle, we looked at the main aspects of Typhoon’s work and prepared to fully apply it in practice. However, in addition to the topics covered, the framework provides a large number of other functions.In this article, we will look at the following Typhoon Framework features:
- Autoinjection (also known as autowiring)
- Automatic selection from alternative implementations of one TyphoonDefinition ,
- Features of working with TyphoonConfig
- Using TyphoonPatcher to organize integration tests ,
- Using runtime attributes when creating objects,
- The implementation of factories based on TyphoonAssembly ,
- Postprocessing objects created with Typhoon,
- Tools for writing asynchronous tests .
Cycle "Manage dependencies in iOS applications correctly"
Autoinjection / Autowire
Often, especially in small projects, there is not enough time to implement the full-layer
TyphoonAssembly level controllers, while the service layer is ready. In such a case, it may be advisable to use autoinjections, also known as autowiring. Let's take a look at a simple example of configuring an email viewing screen:
@interface RCMMessageViewController: UIViewController#import <Typhoon/TyphoonAutoInjection.h> @protocol RCMMessageService; @class RCMMessageRendererBase; @interface RCMMessageViewController : UIViewController @property (strong, nonatomic) InjectedProtocol(RCMMessageService) messageService; @property (strong, nonatomic) InjectedClass(RCMMessageRendererBase) renderer; @end
And currently the only
TyphoonAssembly in the application, specified in
Info.plist :
@implementation RCMHelperAssembly @implementation RCMHelperAssembly - (RCMMessageRendererBase *)messageRenderer { return [TyphoonDefinition withClass:[RCMMessageRendererBase class]]; } - (id <RCMMessageService>)messageService { return [TyphoonDefinition withClass:[RCMMessageServiceBase class]]; } @end
Let's try to run the application with this configuration:

As you can see, the necessary dependencies were automatically substituted. I draw your attention to the fact that when using autoinjection, you do not need to write a method that gives
TyphoonDefinition to
ViewController . It is also worth noting that this approach only works when creating a
UIViewController from
TyphoonStoryboard .
')
A similar approach can be used when writing integration tests - instead of manually creating dependencies of a test object, you can automatically substitute them from a specific
TyphoonAssembly :
@interface RCMMessageServiceBaseTests: XCTestCase #import <Typhoon/TyphoonAutoInjection.h> @interface RCMMessageServiceBaseTests : XCTestCase @property (nonatomic, strong) InjectedProtocol(RCMMessageService) messageService; @end @implementation RCMMessageServiceBaseTests - (void)setUp { [super setUp]; [[[RCMServiceComponentsAssemblyBase new] activate] inject:self]; } - (void)testThatServiceObtainsMessage {
Similarly, dependencies are inserted into a manually created
UIViewController , or from xib.
Like any technology, autowire has both advantages:
- Save time by eliminating the need to implement some of the assembly,
- More informative object interfaces - you can immediately see which dependencies are substituted using Typhoon, which ones are self-contained,
- If any of the automatically inline dependencies of the object are not found in the factory, the crash will occur immediately (in the case of manual substitution, this may go unnoticed).
and disadvantages:
- Binding to Typhoon goes beyond the assembly and affects specific classes,
- After reviewing the structure of the modules TyphoonAssembly project, you can not judge its architecture in general.
The rule of good code that we developed at
Rambler & Co is to spend some time better and prepare well-structured Presentation modules, which will contain definitions for all ViewControllers, and use autowire capabilities only in integration tests. The presence of a well-documented project structure with
TyphoonAssembly greatly exceeds all the advantages of autoinjection.
TyphoonDefinition + Option
In the previous article, we looked at an example of using two different implementations of the same
TyphoonAssembly - combat and fake. However, sometimes this approach is tantamount to shooting sparrows with guns - and Typhoon provides us with much more elegant ways to solve the problem.
Consider another case from
Rambler .
Mail :
The QA team asked to add a special debug-menu to the application, which allows you to work with logs, find out the current build number and other similar things. The settings screen is a table that is collected from the ViewModel collection by a separate class
RCMSettingsConfigurator . This class has two implementations, Base and Debug, which are enabled by the corresponding build scheme. We faced a choice of three options for the implementation of this task:
- Create a configurator manually using #ifdef 's that define the values of the preprocessor directive,
- Write two assembly implementations creating objects for user story settings,
- Use the category TyphoonDefinition + Option .
The first option, of course, is not the choice of real ninjas (well, this is not the case, actively use
#ifdef 's in the code of the mobile application). The second option is the same aforementioned gun aimed at innocent sparrows. The third method, on the one hand, is very simple to implement, on the other, it expands quite flexibly. Let us dwell on it in more detail.
To begin with, we will look at the interface of the category, using the methods of which we can get certain definitions depending on the value of the parameter substituted in the option field:
@interface TyphoonDefinition (Option) @interface TyphoonDefinition (Option) + (id)withOption:(id)option yes:(id)yesInjection no:(id)noInjection; + (id)withOption:(id)option matcher:(TyphoonMatcherBlock)matcherBlock; + (id)withOption:(id)option matcher:(TyphoonMatcherBlock)matcherBlock autoInjectionConfig:(void(^)(id<TyphoonAutoInjectionConfig> config))configBlock; @end
For example, in this case it looks like this:
- (id <RCMSettingsConfigurator>) settingsConfigurator - (id <RCMSettingsConfigurator>)settingsConfigurator { return [TyphoonDefinition withOption:@(DEBUG) yes:[self debugSettingsConfigurator] no:[self baseSettingsConfigurator]]; }
Using the object
TyphoonOptionMatcher allows
you to work with more complex conditions:
- (id <RCMSettingsConfigurator>) settingsConfiguratorWithOption: (id) option - (id <RCMSettingsConfigurator>)settingsConfiguratorWithOption:(id)option { return [TyphoonDefinition withOption:option matcher:^(TyphoonOptionMatcher *matcher) { [matcher caseEqual:@"qa-team" use:[self qaSettingsConfigurator]]; [matcher caseEqual:@"big-bosses" use:[self bigBossesSettingsConfigurator]]; [matcher caseEqual:@"ios-dream-team" use:[self iosTeamSettingsConfigurator]]; [matcher caseMemberOfClass:[RCMConfiguratorOption class] use:[self settingsConfiguratorWithOption:option]]; [matcher defaultUse:[self defaultSettingsConfigurator]]; }]; }
Another possibility is to use the option parameter as the key to find the required
TyphoonDefinition :
- (id <RCMSettingsConfigurator>) settingsConfiguratorWithOption: (id) option - (id <RCMSettingsConfigurator>)settingsConfiguratorWithOption:(id)option { return [TyphoonDefinition withOption:option matcher:^(TyphoonOptionMatcher *matcher) { [matcher useDefinitionWithKeyMatchedOptionValue]; }]; // option = @"debugSettingsConfigurator" definition - debugSettingsConfigurator }
Of course, this feature should not be abused either - if alternative implementations are needed immediately for a large number of objects of the same level of abstraction, it makes sense to replace the whole
TyphoonAssembly module.
TyphoonConfig and TyphoonTypeConverter
In the very first article in one of the examples of using Typhoon, I already mentioned
TyphoonConfig , using it to inject the URL of the server API into one of the network clients. It's time to take a closer look at it.
Supported configuration file formats:
Primitive types (numbers, BOOL, strings) are specified "as is":
{ "config": { "defaultFontSize": 17, "openLinksInExternalBrowser" : NO } }
Typhoon allows you to operate with some other objects:
NSURL ,
UIColor ,
NSNumber ,
UIImage . In this case, a special syntax is used:
{ "config": { "baseURL": NSURL(https:
In addition, if necessary, we can add our own TypeConverter and describe objects of any other class in the configuration file. For example, we want to encapsulate all the details of the style of the application in one object -
RCMStyleModel :
@interface RCMStyleTypeConverter: NSObject <TyphoonTypeConverter> typedef NS_ENUM(NSUInteger, RCMStyleComponent) { RCMStylePrimaryColorComponent = 0, RCMStyleDefaultFontSizeComponent = 1, RCMStyleDefaultFontNameComponent = 2 }; @interface RCMStyleTypeConverter : NSObject <TyphoonTypeConverter> @end @implementation RCMStyleTypeConverter - (NSString *)supportedType { return @"RCMStyle"; } - (id)convert:(NSString *)stringValue { NSArray *styleComponents = [stringValue componentsSeparatedByString:@";"]; NSString *colorString = styleComponents[RCMStylePrimaryColorComponent]; UIColor *primaryColor = [self colorFromHexString:colorString]; NSString *defaultFontSizeString = styleComponents[RCMStyleDefaultFontSizeComponent]; CGFloat defaultFontSize = [defaultFontSizeString floatValue]; NSString *defaultFontName = styleComponents[RCMStyleDefaultFontNameComponent]; UIFont *defaultFont = [UIFont fontWithName:defaultFontName size:defaultFontSize]; RCMStyleModel *styleModel = [[RCMStyleModel alloc] init]; styleModel.primaryColor = primaryColor; styleModel.defaultFontSize = defaultFontSize; styleModel.defaultFont = defaultFont; return styleModel; }
And now we can set the style of the application as follows:
{ "config": { "defaultStyle": RCMStyle(#8732A9;17;HelveticeNeue-Regular), "anotherStyle" : RCMStyle(#AABBCC;15;SanFrancisco) } }
Thus, if several parameters from the configuration file are transferred to the same entity at once, you should think about combining them into a separate model object and writing TypeConverter for it.
TyphoonPatcher
The main difference between integration and unit tests is that in the first case we test the interaction of the individual application modules with each other, and in the second - each specific module in isolation from all the others. So, Typhoon is simply amazing for organizing integration tests.
For example, in our project there is the following chain of dependencies:
RCMPushNotificationCenter -> RCMPushService -> RCMNetworkClientWe want to test the behavior of
RCMPushNotificationCenter depending on the different results of accessing the server. Instead of manually creating a test object, inserting a stub
RCMPushService into it and
replacing implementations of its methods, we can use the already prepared
TyphoonAssembly infrastructure:
- (void) setUp - (void)setUp { [super setUp]; NSArray *collaboratingAssemblies = @[[RCMClientAssembly new], [RCMCoreComponentsAssembly new]]; TyphoonAssembly<RCMServiceComponents> *serviceComponents = [[RCMServiceComponentsAssemblyBase new] activateWithCollaboratingAssemblies:collaboratingAssemblies]; self.pushNotificationCenter = [serviceComponents pushNotificationCenter]; TyphoonPatcher *patcher = [[TyphoonPatcher alloc] init]; [patcher patchDefinitionWithSelector:@selector(networkClient) withObject:^id{ return [RCMFakeNetworkClient new]; }]; }
The
TyphoonPatcher object allows us to patch the method that
renders TyphoonDefinition to any of the
TyphoonAssembly modules. In the block transmitted by
TyphoonPatcher, you can not only transfer another instance of the class, but also use mocks implemented by various frameworks.
Runtime arguments
Typhoon allows instantiating objects not only with predefined dependencies, but also using runtime parameters. It may be necessary, for example, when implementing an abstract factory. Consider an example:
We have
RCMMessageViewController , the mandatory dependency of which is the message object -
RCMMessage :
- (void) setUp @interface RCMMessageViewController : UIViewController - (instancetype)initWithMessage:(RCMMessage *)message; @property (nonatomic, strong) id <RCMMessageService> messageService; @end
The message object is unknown at the time of
TyphoonDefinition's registration when
TyphoonAssembly is activated - so we need to be able to create it on the fly. To do this in
TyphoonAssembly corresponding user story write the following method:
- (UIViewController *) messageViewControllerWithMessage: (RCMMessage *) message - (UIViewController *)messageViewControllerWithMessage:(RCMMessage *)message { return [TyphoonDefinition withClass:[RCMMessageViewController class] configuration:^(TyphoonDefinition *definition) { [definition useInitializer:@selector(initWithMessage:) parameters:^(TyphoonMethod *initializer) { [initializer injectParameterWith:message]; }]; [definition injectProperty:@selector(messageService) with:[self.serviceComponents messageService]]; }]; }
Let's put this method into a separate protocol, for example,
RCMMessageControllerFactory , and inject it into the router:
- (id <RCMFoldersRouter>) foldersRouter - (id<RCMFoldersRouter>)foldersRouter { return [TyphoonDefinition withClass:[RCMFoldersRouterBase class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(messageControllerFactory) with:self]; }]; }
And add to the router the implementation of creating this controller:
- (void) showMessageViewControllerFromSourceController - (void)showMessageViewControllerFromSourceController:(UIViewController *)sourceViewController withMessage:(id <RCMMessageReadableProtocol>)message { RCMMessageViewController *messageViewController = [self.messageControllerFactory messageViewControllerWithMessage:message]; ... }
It is worth mentioning several limitations of this technique:
- Runtime arguments must be objects . Primitives can be wrapped in an NSValue if necessary,
- The objects transferred to the factory must be used in their original form, their state cannot be changed ,
- It should be carefully used in conjunction with cyclic dependencies . Runtime arguments must be passed to all dependency objects, otherwise it will not be solved correctly.
Factory Definitions
In some situations, it is convenient to register
TyphoonDefinition , which can generate other definitions. I will explain with a specific example:
A special factory, RCMTextAvatarFactory, is responsible for creating custom
avatars :
@interface RCMTextAvatarFactory: NSObject @interface RCMTextAvatarFactory : NSObject - (RCMTextAvatar *)avatarWithName:(NSString *)name; @end
Avatars created by this factory must be transferred to other objects. This is implemented as follows - to start, the definition for the factory is registered:
- (RCMTextAvatarFactory *) textAvatarFactory - (RCMTextAvatarFactory *)textAvatarFactory { return [TyphoonDefinition withClass:[RCMTextAvatarFactory class]]; }
And then definitions are recorded for the entities created by this factory:
- (RCMTextAvatar *) textAvatarForUserName: (NSString *) userName - (RCMTextAvatar *)textAvatarForUserName:(NSString *)userName { return [TyphoonDefinition withFactory:[self textAvatarFactory] selector:@selector(avatarWithName:) parameters:^(TyphoonMethod *factoryMethod) { [factoryMethod injectParameterWith:userName]; }]; }
By the way, this feature allows you to seamlessly migrate from using the
service locator , if you have sinned with it, on Typhoon. The first step is to register the locator as a factory, and the second is to implement
TyphoonDefinitions for services using
factoryMethods :
- (id <RCMMessageService>) messageService - (id <RCMMessageService>)messageService { return [TyphoonDefinition withFactory:[self serviceLocator] selector:@selector(messageService)]; }
TyphoonInstancePostProcessor / TyphoonDefinitionPostProcessor
These protocols are used to create so-called infrastructure components. If assembly returns such an object, it is handled differently from ordinary definitions.
Using
TyphoonInstancePostProcessor allows us to
hook in at the time of the return of the created instances by the container and process them in some way. For example, this can be used to log all calls to certain objects, say, to a
networkService :
To begin with, let's write a simple decorator that logs all messages sent to the object:
@interface RCMDecoratedService: NSProxy @interface RCMDecoratedService : NSProxy + (instancetype)decoratedServiceWith:(NSObject <RCMService>*)service; @end @interface RCMDecoratedService() @property (strong, nonatomic) NSObject <RCMService> *service; @end @implementation RCMDecoratedService - (instancetype)initWithService:(NSObject <RCMService> *)service { self.service = service; return self; } + (instancetype)decoratedServiceWith:(NSObject <RCMService>*)service { return [[self alloc] initWithService:service]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.service methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { NSLog(invocation.debugDescription); [invocation invokeWithTarget:self.service]; } @end
Now you need to create an object that implements the
TyphoonInstancePostProcessor protocol - its task will be to determine which of the objects it receives, add additional behavior, and decorate them:
@interface RCMLoggingInstancePostProcessor: NSObject <TyphoonInstancePostProcessor> @interface RCMLoggingInstancePostProcessor : NSObject <TyphoonInstancePostProcessor> @end @implementation RCMLoggingInstancePostProcessor - (id)postProcessInstance:(id)instance { if ([self isAppropriateInstance:instance]) { RCMDecoratedService *decoratedService = [RCMDecoratedService decoratedServiceWith:instance]; return decoratedService; } return instance; } - (BOOL)isAppropriateInstance:(id)instance { if ([instance conformsToProtocol:@protocol(RCMService)]) { return YES; } return NO; } @end
And the last step is to register the
RCMLoggingInstancePostProcessor in one of the
TyphoonAssembly . The object itself does not participate in the dependency injection process and lives on its own. Its life cycle is tied to the time of the
TyphoonComponentFactory life.
@implementation RCMApplicationAssembly @implementation RCMApplicationAssembly - (id)loggingProcessor { return [TyphoonDefinition withClass:[RCMLoggingInstancePostProcessor class]]; } ... @end
Now all dependencies created by Typhoon will pass through
RCMLoggingInstancePostProcessor - and those who implement the
RCMService protocol
will turn around in
NSProxy .
Another infrastructure component,
TyphoonDefinitionPostProcessor , allows all registered definitions to be processed before the objects described by them are created. Thus, we can in any way configure and re-compile the
TyphoonDefinitions transferred to such a processor:
- (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory;
As examples of using this component, you can cite the
TyphoonPatcher and
TyphoonConfigPostProcessor already mentioned in the article.
Asynchronous testing
For those who for some reason cannot or do not want to use
XCTestExpectation , Typhoon offers its own set of methods to implement asynchronous call testing. Consider as an example a synchronization test for mail collectors:
- (void) testThatServiceSynchronizeMailBoxesList - (void)testThatServiceSynchronizeMailBoxesList { // given NSInteger const kExpectedMailBoxCount = 4; [OHHTTPStubs stubRequestsPassingTest:REQUEST_TEST_YES withStubResponse:TEST_RESPONSE_WITH_FILE(@"mailboxes_success")]; __block NSInteger resultCount; __block NSError *responseError = nil; // when [self.mailBoxService synchronizeMailBoxesWithCompletionBlock:^(NSError *error) { responseError = error; NSFetchedResultsController *controller = [self.mailBoxService fetchedResultsControllerWithAllMailBoxes]; resultCount = controller.fetchedObjects.count; }]; // then [TyphoonTestUtils waitForCondition:^BOOL{ typhoon_asynch_condition(resultCount > 0); } andPerformTests:^{ XCTAssertNil(responseError); XCTAssertEqual(resultCount, kExpectedMailBoxCount); }]; }
The standard timeout added by the developers is seven seconds, the condition is checked every second. If it is not executed, the test will fail with the corresponding exception. If necessary, you can use your own timeout:
TyphoonTestUtils wait: 30.0f secondsForCondition: ^ BOOL [TyphoonTestUtils wait:30.0f secondsForCondition:^BOOL{ typhoon_asynch_condition(resultCount > 0); } andPerformTests:^{ XCTAssertNil(responseError); XCTAssertEqual(resultCount, kExpectedMailBoxCount); }];
Conclusion
In this article, we looked at a large number of different types of Typhoon Framework — autoinjection, the use of configuration files, helpers for integration testing, and much more. Possession of these techniques will allow you to solve more problems without inventing your bikes, even if they will not be used every day.
In the next part of the cycle, we briefly review two other implementations of Dependency Injection Containers for Cocoa —
Objection and
BloodMagic . And finally, a little news - my colleague German Saprykin and I joined the Typhoon development team, so the framework has become a little more domestic.
Cycle "Manage dependencies in iOS applications correctly"
useful links