📜 ⬆️ ⬇️

Manage dependencies in iOS applications correctly: Typhoon device



In the last part of the cycle, we met the Dependency Injection framework for iOS - Typhoon , and looked at basic examples of its use in the Rambler project. Mail. This time we delve into the study of its internal structure.

Cycle "Manage dependencies in iOS applications correctly"



Introduction


To begin with, let's analyze a small glossary of terms that will be actively used in this article:

And once again I emphasize that it is very important to understand the basic principles of the framework - after that we can safely move on and explore all the other Typhoon buns, without dwelling on the details of their implementation.

In order not to clutter up the article with huge code listings, I will periodically refer to certain framework files, but bring only the most interesting moments. I draw your attention to the fact that the current version of Typhoon Framework at the time of this writing - 3.1.7 .
')

Initialization


The life cycle of an application using Typhoon is as follows:



It is in setDelegate : the creation and activation of the start assemblies takes place.

Automatic loading of factories is possible in two cases: we either specified their classes in Info.plist under the key TyphoonInitialAssemblies :
+ (id) factoryFromPlistInBundle: (NSBundle *) bundle
+ (id)factoryFromPlistInBundle:(NSBundle *)bundle { TyphoonComponentFactory *result = nil; NSArray *assemblyNames = [self plistAssemblyNames:bundle]; NSAssert(!assemblyNames || [assemblyNames isKindOfClass:[NSArray class]], @"Value for 'TyphoonInitialAssemblies' key must be array"); if ([assemblyNames count] > 0) { NSMutableArray *assemblies = [[NSMutableArray alloc] initWithCapacity:[assemblyNames count]]; for (NSString *assemblyName in assemblyNames) { Class cls = TyphoonClassFromString(assemblyName); if (!cls) { [NSException raise:NSInvalidArgumentException format:@"Can't resolve assembly for name %@", assemblyName]; } [assemblies addObject:[cls assembly]]; } result = [TyphoonBlockComponentFactory factoryWithAssemblies:assemblies]; } return result; } 

or implemented the -initialFactory method in our AppDelegate :
+ (TyphoonComponentFactory *) factoryFromAppDelegate: (id) appDelegate
 + (TyphoonComponentFactory *)factoryFromAppDelegate:(id)appDelegate { TyphoonComponentFactory *result = nil; if ([appDelegate respondsToSelector:@selector(initialFactory)]) { result = [appDelegate initialFactory]; } return result; } 


If neither one nor the other has been done, the assembly will have to be created by hands elsewhere in the code, which is not recommended.

You can find out more about the initialization details of Typhoon in the following source files:

Activation


This process is key to the framework. By activation is meant the creation of an object of the class TyphoonBlockComponentFactory , the instance of which is “under the hood” of all activated assembly. Thus, any assembly plays the role of an interface for communicating with a real factory.

Let's see what happens, without really going into details:
  1. In TyphoonBlockComponentFactory , the initializer -initWithAssemblies: is called, and the assembly array is passed to the input, which must be activated.
  2. Each of the definitions created by the activated assembly is assigned its own unique key (random string + method name).
  3. All TyphoonDefinition is added to the registry array of the newly created TyphoonBlockComponentFactory .



Of course, the matter is not limited to these three points: aspects are added for each registered TyphoonDefinition , getters of all dependencies are sorted, creating an initialization chain of the object graph, and pools of instances are created in TyphoonBlockComponentFactory - in general, a large number of different actions are performed to ensure the framework works. In this article, we will not go into the details of each of the procedures in question, as this may distract from understanding the general working principles of Typhoon.

We looked at how TyphoonAssembly is activated - it remains to understand why this should be done at all. Every time we manually pull some method from the assembly that gives TyphoonDefinition to the dependency, the following happens:
- (void) forwardInvocation: (NSInvocation *) anInvocation
 - (void)forwardInvocation:(NSInvocation *)anInvocation { if (_factory) { [_factory forwardInvocation:anInvocation]; } ... } 

In _factory, the resulting NSInvocation is processed and converted to a call to the following method:
- (id) componentForKey: (NSString *) key args: (TyphoonRuntimeArguments *) args
 - (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args { if (!key) { return nil; } [self loadIfNeeded]; TyphoonDefinition *definition = [self definitionForKey:key]; if (!definition) { [NSException raise:NSInvalidArgumentException format:@"No component matching id '%@'.", key]; } return [self newOrScopeCachedInstanceForDefinition:definition args:args]; } 

The key generated from the selector method gets one of the TyphoonBlockComponentFactory definitions registered in the TyphoonBlockComponentFactory definition, and then a new instance is created on its basis, or the cached is reused.

You can learn more about the activation process in the following source files:

Work with Storyboard


Under the hood, Typhoon uses its UIStoryboard subclass - TyphoonStoryboard . The first feature that catches the eye is the factory method, which differs from its parent by an additional parameter — factory:
 + (TyphoonStoryboard *)storyboardWithName:(NSString *)name factory:(id<TyphoonComponentFactory>)factory bundle:(NSBundle *)bundleOrNil; 

It is in this factory that implements the TyphoonComponentFactory protocol that the definitions for the current storyboard screens will be searched. Let's look at all the steps of dependency injection in ViewControllers:
  1. First of all we get to the -instantiateViewControllerWithIdentifier: method, redefined in TyphoonStoryboard .
  2. An instance of the desired controller is created by calling super'a.
  3. Injection of all dependencies of the current controller and its child controllers is initiated:
    - (void) injectPropertiesForViewController: (UIViewController *) viewController
     - (void)injectPropertiesForViewController:(UIViewController *)viewController { if (viewController.typhoonKey.length > 0) { [self.factory inject:viewController withSelector:NSSelectorFromString(viewController.typhoonKey)]; } else { [self.factory inject:viewController]; } for (UIViewController *controller in viewController.childViewControllers) { [self injectPropertiesForViewController:controller]; } } 
  4. In TyphoonBlockComponentFactory , a procedure that is already familiar to us takes place — the type TyphoonDefinition corresponding to the current class is searched for and the dependency graph is injected into it.

Now I will not dwell on the specific implementation of working with TyphoonStoryboard in the application - this topic will be covered in one of the following articles.

More information about the implementation of the storyboard can be found in the following source files:

TyphoonDefinition


In almost every snippet I have mentioned, in one form or another, the class TyphoonDefinition is found. As I mentioned when enumerating terms, TyphoonDefinition is a kind of configuration class for the dependency being created - therefore, its interface is of particular interest to us:

Of all the above properties, the scope of the object deserves special attention.

More information about how TyphoonDefinition works can be found in the following source files:

TyphoonScope


It is important to clearly understand that, speaking of the different types of the object's life cycle, we are still rigidly tied to the lifetime of the TyphoonBlockComponentFactory instance used - if this factory is freed from memory, all object graphs will be released along with it.
Let's see what each of the TyphoonScope values leads to :

The created object, depending on the scope property of its configuration, is stored in one of the TyphoonComponentFactory pools, each of which works in a certain way.

More about the principles of the Typhoon dependency cache can be found in the following sources:

Conclusion


We managed to consider only the most basic working principles of Typhoon Framework - initialization, factory activation, the device TyphoonAssembly , TyphoonStoryboard , TyphoonDefinition and TyphoonBlockComponentFactory , the life cycle features of the objects created. The library contains a lot of interesting concepts, the implementation of which sometimes just fascinates.

I strongly recommend spending a few days and digging in their study with your head - this is more than a worthy alternative to studying the many lessons in the style “We work in Xcode with the Swift mouse for free and without SMS” and “Advanced animation of the file upload indicator” .

In the next part of the cycle, you will learn how to avoid the emergence of one huge factory, properly break the Assembly-level architecture into modules and cover the whole thing with tests.

Cycle "Manage dependencies in iOS applications correctly"



useful links


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


All Articles