Background: The dependency management of the iOS project of my company is relatively primitive, but the APP functions are increasingly complex, which brings many problems, such as long compilation time during development, serious coupling between modules, chaotic module dependency, etc. Recently, WE heard that some functions of this project may need to be independently created into a new APP. In line with the principle of Don’t repeat yourself, we tried to separate all modules from the original project and integrate these modules into the new APP.

Recently, I have preliminarily completed the modularization of the new APP, and I have summarized some experience to share with you.

Module partition

Modularization still needs to be combined with the actual business, and the functions of the current APP should be divided into modules. When dividing modules, we also need to pay attention to the levels between modules.

For example, in our project, modules are divided into three levels: base, middle, and business. Base layer modules such as network framework, persistence, Log, social sharing modules, this layer of modules we can call components, have a strong reusable. The module of the middle layer can have login module, network layer, resource module, etc. One of the characteristics of the module of this layer is that they rely on the basic components but do not have strong business attributes. At the same time, the business layer is very dependent on the module of this layer. Business layer modules are modules directly corresponding to product requirements, such as business functions such as moments, live broadcasts and Feeds streams.

Code isolation

Modularity first of all is code level independence, any base module can be compiled independently, the bottom module must not have the code dependence on the upper module, but also to ensure that such code will not appear in the future.

In this case, we chose to use CocoaPods to ensure code isolation between modules. Base and middle tier modules are always made into standard private Pods components and added to the private Pods repository. Modules at the business level do not have to be added to the private Pods repository. The subModule + Local Pods solution can also be used. There are two reasons for this. First, business modules tend to change frequently, requiring frequent pod install or POD update for standard private Pods components. Second, if it is a Local pod, it will directly reference the source file of the corresponding repository. The change of the business module under the Pods project in the main project is a direct change to its Git repository, without frequent POD repo push and POD install operations.

Dependency management

Another important reason to use CocoaPods is that you can use it to manage dependencies between modules. One of the reasons it was difficult to reuse functionality in the project was that dependencies were not declared. The dependencies are not just for module A to depend on module B, but for all the engineering configurations required for module A to run, such as adding A GCC_PREPROCESSOR_DEFINITIONS preprocessor macro to compile properly. For this reason, I think module dependency declarations are very important, and even without a management tool like CocoaPods, there should be documentation that explains the dependencies of each internal module or SDK.

The convenience of CocoaPods is that you cannot pass the Pod Spec Lint process unless you list your module’s dependencies, and all dependencies must be pods repositories. In addition, the integration of dependencies is automated, as CocoaPods automatically adds project configurations and dependency components.

Module integration

After completing the above two steps, the construction of modular engineering is almost complete, so let’s look at how to make better use of these modules in engineering. We wrote a componentized open source solution TinyPart [https://github.com/RyanLeeLY/TinyPart].

In general, module initialization needs to be done around APP startup or UI initialization. Sometimes the startup order of each module may be different. This initialization logic is often added to the AppDelegate class. After a while, the AppDelegate class becomes bloated, logically complex, and difficult to maintain. In TinyPart, the Module’s declaration protocol includes a UIApplicationDelegate, which means that each Module can implement its own UIApplicationDelegate protocol, and the order in which they are called is customizable.

@interface TPLShareModule : NSObject <TPModuleProtocol> @end @implementation TPLShareModule TP_MODULE_ASYNC TP_MODULE_PRIORITY(TPLSHARE_MODULE_PRIORITY) - (void)moduleDidLoad:(TPContext *)context { [WXApi registerApp:APPID]; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { return [WXApi handleOpenURL:url delegate:self]; } - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id>  *)options { return [WXApi handleOpenURL:url delegate:self]; } @endCopy the code

The above code is the initialization content of a wechat social sharing module and implements the methods in UIApplicationDelegate required by wechat sharing.

communication

The message

Message is a very important concept in object orientation, it is an important way that objects communicate before. However, if we want to send a message to an object in OC, it is normal to import the header file of the object class so that we can write code like aInstance Method.

In modularity, however, we don’t want modules to refer to each other’s class files, but we want to communicate. What happens? Do it by agreement. OC is a dynamic language. Methods are called dynamically, and message methods are declared in header files only to pass static checks before compilation. In other words, we simply write a protocol to tell the compiler that there is such a method, and whether or not there is such a method will be known after the message is sent. Since OC has this feature, we can even send messages directly to an object by class name and method name, which is how most componentized routing is implemented on the web.

So in TinyPart we provide both protocol and routing modes to invoke services within a module.

@protocol TestModuleService1 <TPServiceProtocol>
- (void)function1;
@end

@interface TestModuleService1Imp : NSObject <TestModuleService1>
@end

@implementation TestModuleService1Imp
TPSERVICE_AUTO_REGISTER(TestModuleService1) // Service will be registered in "+load" method

- (void)function1 {
    NSLog(@"%@", @"TestModuleService1 function1");
}
@end
Copy the code

In the code above, we define a protocol for a service.

#import "TestModuleService1.h"

id<TestModuleService1> service1 = [[TPServiceManager sharedInstance] serviceWithName:@"TestModuleService1"];

[service1 function1];
Copy the code

All we need to do here is import the protocol header and send a message to TestModuleService1.

As we saw in the cross-module invocation scheme above, only one protocol file is exposed. Let’s look at how to route without exposing any header files at all.

#import "TPRouter.h"

@interface TestRouter : TPRouter
@end

@implementation TestRouter
TPROUTER_METHOD_EXPORT(action1, {
    NSLog(@"TestRouter action1 params=%@", params);
    return nil;
});

TPROUTER_METHOD_EXPORT(action2, {
    NSLog(@"TestRouter action2 params=%@", params);
    return nil;
});
@end
Copy the code

Here we refer to ReactNative’s scheme, using a TPROUTER_METHOD_EXPORT macro to define a routing service that can be called, and pass in a params parameter. And then we call the route.

[[TPMediator sharedInstance] performAction:@"action1" router:@"Test" params:@{}];
Copy the code

notice

In addition to the two common module communication schemes mentioned above, we find that there are often cross-module NSNotification in projects, and it is the most convenient way to implement this observer mode using NSNotification. Although NSNotification can achieve module decoupling, too loose management of notifications will lead to the complexity of NSNotification logic scattered in each module, so we added a directed communication scheme for TinyPart.

Directed communication limits the propagation direction of notifications based on NSNotification. Notifications sent by lower-layer modules to upper-layer modules are called Broadcast, and notifications sent by upper-layer modules to lower-layer modules or modules of the same layer are called Report. This has two advantages: on the one hand, it facilitates the maintenance of notifications; on the other hand, it helps us to divide the module hierarchy. If we find that a module needs to Report to multiple modules at the same level, it is likely to be divided into modules at a lower level.

The usage is similar to that of NSNotification, except that the method to create the notification is a chained call, something like this:

TPNotificationCenter *center2 = [TestModule2 tp_notificationCenter]; [center2 reportNotification:^(TPNotificationMaker *make) { make.name(@"report_notification_from_TestModule2"); } targetModule:@"TestModule1"]; [center2 broadcastNotification:^(TPNotificationMaker *make) { make.name(@"broadcast_notification_from_TestModule2").userInfo(@{@"key":@"value"}).object(self); }]; // receive TPNotificationCenter *center1 = [TestModule1 tp_notificationCenter]; [center1 addObserver:self selector:@selector(testNotification:) name:@"report_notification_from_TestModule2" object:nil];Copy the code

reference

BeeHive

ReactNative