SoFunction
Updated on 2025-04-12

A brief analysis of iOS modular development

Background: Since the dependency management of the iOS project in the company is in a relatively primitive state, but the APP functions are becoming more and more complex, this brings many problems, such as too long compilation time during development, serious coupling between modules, and chaotic module dependence. Recently I heard that some functions in this project may need to separate a new APP. Based on the principle of "Don't repeat yourself", we try to extract the modules from the original project and integrate these modules in the new APP.

Recently, the modularization of the new APP has been initially completed, and it can be regarded as summarizing some experiences from it and sharing them. At the same time, a modular framework has been completed. TinyPart welcomes star.

Module division

To do modularization, you should combine the actual business and divide the current APP functions into modules. When dividing modules, you also need to pay attention to the levels between modules.

For example, in our project, the module is divided into three levels: basic layer, intermediate layer, and business layer. Basic layer modules such as network frameworks, persistence, Log, and social sharing. We can call this layer modules components and have strong reusability. The intermediate layer modules can include login modules, network layer, resource modules, etc. One feature of this layer module is that they rely on basic components but do not have strong business attributes. At the same time, the business layer has a strong dependence on this layer of modules. The business layer module is a module that directly corresponds to product requirements, such as business functions such as Moments, Live Streaming, and Feeds Streaming.

Code isolation

The first thing to do in modularization is to be independent at the code level. Any basic module can be compiled independently. The underlying module must not have code dependence on the upper module, and it is also necessary to ensure that such code will not appear again in the future.

Here we choose to use CocoaPods to ensure code isolation between modules. The basic and intermediate layer modules will definitely be made into standard private pods components and added to the private pods repository. The business layer modules do not have to be added to the private pods repository, but you can also use the submodule + local pods solution. There are two reasons for this: one is that the changes in business modules are often frequent. If it is a standard private pods component, it needs to frequently operate pod install or pod update; the other is that if it is a local pod, it will directly refer to the source file of the corresponding warehouse. The changes in the business module under the main project to the pods project are to directly change its git warehouse, and there is no frequent pod repo push and pod install operations.

Dependency management

Another important reason for choosing to use CocoaPods is that it can manage dependencies between modules. One of the important reasons why various functions of the previous project were difficult to reuse was that there was no declaration of dependencies. The dependency here is not only the thing that A module depends on module B, but also all the engineering configurations required for module A to run. For example, the A module project requires adding a GCC_PREPROCESSOR_DEFINITIONS preprocessing macro to be compiled normally. Therefore, I personally think that module dependency declaration is very important. Even if there is no management tool like CocoaPods, there should be relevant documents to explain the dependencies of each internal module or SDK.

The convenience of CocoaPods is that you must list the dependencies of your module, otherwise you will not be able to pass the pod spec lint process, and all dependencies must be pods repository. In addition, dependency integration is also automated, and CocoaPods can automatically add engineering configurations and dependency components.

Module integration

After completing the above two steps, the construction of the modular project is basically over. Next, let’s discuss how to better use these modules in the project. For this purpose, we wrote a componentized open source solution, TinyPart.

Generally speaking, module initialization needs to be completed at a time near the APP startup or UI initialization. Sometimes the startup sequence of each module may also be of particular importance. We often add these initialization logic to the AppDelegate class. After a while, we will find that the AppDelegate class has become bloated, complex logic and difficult to maintain. In TinyPart, the Module's declaration protocol includes UIApplicationDelegate, which means that each module can implement its own set of UIApplicationDelegate protocols, and the order of call between them can be customized.

@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];
}
@end

The above code is the initialization content of a WeChat social sharing module, and at the same time implements the method in UIApplicationDelegate required by WeChat sharing.

Communication

information

In object-oriented, message is a very important concept and it is an important way for objects to communicate before them. However, if you want to send a message to an object in the OC, the normal way is to import the header file of the object class, so that we can write code like [aInstance method].

However, in modularization, we do not want modules to refer to each other's class files, but we also want to achieve communication. What should we do? Completed through an agreement. We know that OC is a dynamic language, and the method calling process is actually dynamic, and the declaration of message methods in the header file is only for passing static checks before compilation. In other words, we just need to write a protocol to tell the compiler that there is such a method. As for whether there is actually this method, it will be known after the message is sent. Since OC has this feature, we can even send messages to an object directly through class names and method names. This is actually the implementation mechanism of most componentized routing on the Internet.

Therefore, in TinyPart we provide both protocol and routing modes to call services within the 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

In the above code, we define a service protocol.

#import ""

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

[service1 function1];

Here we only need to import the header file of the above protocol, and then we can send a message to TestModuleService1.

We see that in the above cross-module call scheme, you only need to expose one protocol file. Let’s take a look at how to use routing to avoid exposing any header files at all.

#import ""

@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

Here we refer to the ReactNative solution, and use a TPROUTER_METHOD_EXPORT macro to define a routing service that can be called, and at the same time, a params parameter can be passed into it. Then we call this route again.

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

notify

In addition to the two common module communication solutions mentioned above, we found that there are often cross-module NSNotification in projects, and it is the most convenient way to use NSNotification for such observer mode. Although NSNotification can achieve inter-module decoupling, too loose management of notifications will cause the NSNotification logic scattered across various modules to become very complex, so we have added a directed communication solution to TinyPart.

Directed communication means that the propagation direction of notifications is restricted based on NSNotification. The notification of the underlying module to the upper module is called a broadcast Broadcast, and the notification of the upper module to the underlying module or the same-layer module is called a report report. This has two benefits: on the one hand, it is more conducive to the maintenance of notifications, and on the other hand, it can help us divide the module levels. If we find that there is a module that needs to be reported to multiple modules of the same level, then this module is likely to be divided into a lower-level module.

The usage is similar to NSNotification, except that the method of creating a notification is a chain call, which is probably like this:

// sendTPNotificationCenter *center2 = [TestModule2 tp_notificationCenter];

[center2 reportNotification:^(TPNotificationMaker *make) {
  (@"report_notification_from_TestModule2");
} targetModule:@"TestModule1"];
  
[center2 broadcastNotification:^(TPNotificationMaker *make) {
  (@"broadcast_notification_from_TestModule2").userInfo(@{@"key":@"value"}).object(self);
}];

// take overTPNotificationCenter *center1 = [TestModule1 tp_notificationCenter];
[center1 addObserver:self selector:@selector(testNotification:) name:@"report_notification_from_TestModule2" object:nil];