📜 ⬆️ ⬇️

Managing dependencies in iOS applications correctly: Getting to know Typhoon

"Any magic, is sufficiently analyzed is indistinguishable from technology."

Arthur Clark
(the epigraph in the official wiki project Typhoon Framework)



Cycle "Manage dependencies in iOS applications correctly"



Introduction


As part of this series of articles, I will not delve into theory, consider the Dependency Inversion Principle or the patterns of Dependency Injection - we take it for granted that the reader is already sufficiently prepared to learn Zen and go straight to the practice (links for an introduction to theory are given in the very end of the post).
')
Typhoon Framework is the most famous and popular implementation of the DI container for Objective-C and Swift applications. The project is quite young - the first commit was made at the very end of 2012, but already got a lot of fans . Special mention deserves the active support of the project by its creators (one of whom, by the way, lives and works in Omsk) - most of the new Issues are answered within ten minutes, and after a few hours the entire team joins the discussion.

Why do we need Typhoon? I will answer with one abbreviation - IoC. I have already promised not to go into theory, so I simply refer to Martin Fowler .



Let's look at a few of Typhoon’s main advantages, which should be enough to attract the attention of any iOS developer:


Basic integration with the project


To show how easy Typhoon integrates into a clean application, consider the case in which we want to embed a startUpConfigurator object into AppDelegate , which can correctly configure our application.

@interface RIAppDelegate @property (strong, nonatomic) id <RIStartUpConfigurator> startUpConfigurator; @end 

  1. Create your own subclass TyphoonAssembly (which is our DI container):

     @interface RIAssembly : TyphoonAssembly - (RIAppDelegate *)appDelegate; @end 

    In fact, this method can not be declared in the interface - but for educational purposes we will leave it here.

  2. Implement RIAssembly implementation:

     @implementation RIAssembly - (RIAppDelegate *)appDelegate { return [TyphoonDefinition withClass:[RIAppDelegate class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(startUpConfigurator) with:[self startUpConfigurator]]; } } - (id <RIStartUpConfigurator>)startUpConfigurator { return [TyphoonDefinition withClass:[RIStartUpConfiguratorBase class]]; } @end 

    Note that in the method that returns the TyphoonDefinition for the configurator, no additional injections are made - but nothing prevents this in the future. For example, we can pass it the keyWindow of the application so that we can set the rootViewController .

  3. DI containers, by their definition, should be as automated as possible , we don’t want to manually request something from TyphoonAssembly . The best option is to use the Info.plist file. All that is required of us is to add under a certain key the names of the classes of the factories that should be activated when the application starts.



    This completes the configuration. Let's see what we ended up with.

  4. Put a breakpoint in the -applicationDidFinishLaunching method and run the application:



    The configurator successfully promoted the class we need (I remind you that RIAppDelegate itself will work with it according to the protocol defined by us).

As you can see, Typhoon's basic integration takes only a couple of minutes. Moreover, the framework can be embedded in an application that has already been written for a long time - but in this case the degree of pleasure will depend on the quality of the code design.

But who is interested in the instructions for setting up - let's better look at the real cases of using Typhoon in the Rambler project. Mail .

Typhoon usage examples in Rambler. Mail


  1. To create a simple instance that implements a specific protocol, you only need to specify the class of the object being created.

     - (id <RCMStoryboardBuilder>)storyboardBuilder { return [TyphoonDefinition withClass:[RCMStoryboardBuilderBase class]]; } 

  2. RCMAuthorizationPopoverBuilderBase requires a storyboardBuilder object, which we have already learned how to create. To inject it into the dependency graph, we just need to call the appropriate method - [self storyboardBuilder] . Thus, we not only created an instance of the class, but also established all its dependencies.

     - (id <RCMPopoverBuilder>)authorizationPopoverBuilder { return [TyphoonDefinition withClass:[RCMAuthorizationPopoverBuilderBase class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(storyboardBuilder) with:[self storyboardBuilder]]; }]; } 

  3. We want the RCMNetworkLoggerBase class object to be a singleton — the TyphoonDefinition scope property, which is responsible for setting the object's life cycle, helps us with this.

     - (id <RCMLogger>)networkLogger { return [TyphoonDefinition withClass:[RCMNetworkLoggerBase class] configuration:^(TyphoonDefinition *definition) { definition.scope = TyphoonScopeSingleton; }]; } 

  4. Let's see how Typizer implements Initializer Injection . To work, the settings service requires two mandatory dependencies - the networkClient , which can work with network requests, and credentialsStorage , which stores various user credentials. The -useInitializer method of TyphoonDefinition takes a selector of a particular init and a block in which its parameters are embedded into the initializer.

     - (id <RCMSettingsService>)settingsService { return [TyphoonDefinition withClass:[RCMSettingsServiceBase class] configuration:^(TyphoonDefinition *definition) { [definition useInitializer:@selector(initWithClient:sessionStorage:) parameters:^(TyphoonMethod *initializer) { [initializer injectParameterWith:[self mailXMLRPCClient]]; [initializer injectParameterWith:[self credentialsStorage]]; }]; }]; } 

  5. Now we will study the implementation of Method Injection. Error service can send received NSError to all signed handlers. So that all the unprocessed errors are recorded in the log, we want to sign the standard handler immediately after creating the service.

     - (id <RCMErrorService>)errorService { return [TyphoonDefinition withClass:[RCMErrorServiceBase class] configuration:^(TyphoonDefinition *definition) { [definition injectMethod:@selector(addErrorHandler:) parameters:^(TyphoonMethod *method) { [method injectParameterWith:[self defaultErrorHandler]]; }]; }]; } 

  6. All ViewControllers of an application must have three dependencies — an error handling service that feeds all received NSError objects, a basic error handler that can properly handle certain common error codes, and a basic router. To avoid duplication of this code for all controllers, we injected these three dependencies into the base TyphoonDefinition . To use it in other methods, you only need to set the parent property.

     - (UIViewController *)baseViewController { return [TyphoonDefinition withClass:[UIViewController class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(errorService) with:[self.serviceComponents errorService]]; [definition injectProperty:@selector(errorHandler) with:[self baseControllerErrorHandler]]; [definition injectProperty:@selector(router) with:[self baseRouter]]; }]; } - (UIViewController *)userNameTableViewController { return [TyphoonDefinition withClass:[RCMMessageCompositionViewController class] configuration:^(TyphoonDefinition *definition) { definition.parent = [self baseViewController]; [definition injectProperty:@selector(router) with:[self settingsRouter]]; }]; } 

    It is worth noting that Typhoon allows you to build a much more complex chain of inheritance TyphoonDefinition'ov.

  7. Instead of hardcoding the URLs of our API, we store them in a configuration plist file. In this situation, Typhoon helps with the following - it loads the required file and converts its fields into native objects (in this case, NSURL ), providing us with a convenient syntax for accessing the config fields - TyphoonConfig (KEY) .

     - (id)configurer { return [TyphoonDefinition configDefinitionWithName:RCMConfigFileName]; } - (id<RCMRPCClient>)idXMLRPCClient{ return [TyphoonDefinition withClass:[RCMRPCClientBase class] configuration:^(TyphoonDefinition *definition) { [definition useInitializer:@selector(initWithBaseURL:) parameters:^(TyphoonMethod *initializer) { [initializer injectParameterWith:TyphoonConfig(RCMAuthorizationURLKey)]; }]; }]; } 


Myths


Among those who were interested in the Typhoon Framework only superficially, there are several myths that have little or no basis.

  1. High entry threshold
    In fact, within a few hours of digging into the sources, documentation, and sample projects (of which, by the way, there are already three), you can understand the basic working principles of Typhoon and start using it in your project. If there is no desire to go deep - you can do with examples and, without delay, integrate the framework into your code.

  2. Strong influence on debugging
    There are actually not many actual points of contact between our code and Typhoon, and the developers have already provided informative Exceptions at these junctions.

  3. If Typhoon ceases to support, do not cut it out of the project
    Typhoon is good because we almost never interact directly with it anywhere - so it will be difficult to refuse it, but it’s still possible - just write your own level of factories and mechanisms for integrating it with code (even if it’s manual).

  4. But ... same swizzling!
    Objective-C is famous for its runtime, and not using its capabilities in such a framework is at least silly. In addition, we use the component as a “black box”, relying on the fact that the same 1200 stars have already stepped on all the rakes.

  5. Why do I need Typhoon when I can write my bike?
    Typhoon Framework seriously reduces the amount and complexity of the code responsible for creating the object graph and passing dependencies when navigating between screens. In addition, it provides us with centralized dependency management without the drawbacks of a self-hosted service locator. A simple factory, I think, can be written by any reader, but as the project progresses, you will continue to face the limitations of this approach - and you will have to implement what you have long thought up and debugged, or sacrifice the functionality and productivity of your project.

Conclusion


Initially, I planned to completely translate my performance on Rambler.iOS to a printed form - but, having written a couple of sections, I realized that there is too much material for one article. Therefore, in the following series:

Cycle "Manage dependencies in iOS applications correctly"



useful links


Source: https://habr.com/ru/post/258325/


All Articles