preface

There have been many articles about the componentization scheme of iOS development, but few can introduce how to implement it in the project. This article is the author’s experience summarized after implementing the componentization scheme in the actual project. This article will not discuss much theoretical knowledge, but will focus on implementation.

1 Component implementation tools

To implement business componentization, each business module is individually packaged into Pods and then integrated as a component through CocoaPods in the main project. Componentization relies on Git and CocoaPods, so you need to install Git and CocoaPods on your macOS and have a Git server ready before you start. This article uses Github as an example, and the steps won’t be much different with other Git services.

1.1 Creating an Organization

In real development, there is one Git repository for each business module and one Pod for each business Git repository, and it is recommended that all repositories be placed in one organization. Create an organization on Github as shown in the figure.

Once created, create your repository of business modules in the organization and invite your fellow developers into the organization.

1.2 Create a private Pods repository

CocoaPods has a public, open default Pods repository, which is a repository of open source iOS component libraries that developers can use on Github. But for corporate projects, the code is not open to the public, so you can only use private Pods, so you need to build your own private Pods repository to store those private Pods. Create an empty repository in your organization, as shown in the figure.

Once the repository is created, you need to add a local private Pods repository and link to the remote Git repository. Open the macOS command line and type:

pod repo add ModularizationPod https://github.com/iOSShop/ModularizationPod.git
Copy the code

When we’re done we go to the CocoaPods directory:

cd ~/.cocoapods/repos/
open .
Copy the code

You can see two repositories in the directory. Master is the public Pods repository for CocoaPods, and ModularizationPod is the private Pods repository we created.

The next step is to add a.gitignore file to the ModularizationPod repository, which can be copied directly from the master repository.

CD to warehouse directory in command terminal:

cd /Users/caicai/.cocoapods/repos/ModularizationPod
Copy the code

Then we need to commit to a remote Git repository:

git add .
git commit -m "first commit"
git push
Copy the code

During the process, you may need to enter the account password to verify the Git identity. After the verification, you can view it in the remote Git repository.

In the future, this remote Git repository will hold pods information for our business module.

2 Component Scheme description

This paper describes the implementation of componentization scheme with a simple mall business, including how to realize the componentization of business modules, how to call between modules and how to communicate between modules. We take the four modules of basic account, goods, order and payment as examples.

2.1 How to Split Service Modules

Componentization of service modules is to separate services and reduce the coupling between service modules. For example, in the product details page [click to buy], the next step is to enter the order generation page. The traditional way is to import the ViewController of the order generation page directly from the ViewController of the product details page. And then instantiate the ViewController and pass a value and just push it. As the project grows larger and the business logic becomes more complex, this direct introduction of code files can lead to stronger dependencies between modules, or even more complex dependencies. Even though there are only four modules in the example, there are many dependencies among them, as shown in the figure below:

This is a general software engineering problem, not limited to iOS development. The solution, too, was simple: provide a Mediator. Business modules do not directly reference each other, but indirectly form a reference relationship through Mediator. In addition, mediators can provide the business that the module needs to expose for other modules to call, and if they do not need to be exposed, mediators will not be introduced. For example, the account module has a login page and a registration page. In actual scenarios, the login page may only be invoked by other service modules. The registration page only needs to be skipped from the login page and does not need to be invoked by other service modules. As shown below:

2.2 Solve the invocation between modules as a service

The man-in-the-middle split of business modules only makes logic a little clearer. In fact, it is still introducing business code files, and the calls between business and business are still not clear. For example, when a login page pops up on the order page, the developer of the order module needs to find the UIViewController file of the login page, import it, instantiate the object, and present or push the page. A more complex business might also need to verbally or documentically tell the caller how to use the class file, how to pass parameters, and so on. The developer thinks THAT all I need is a UIViewController instantiation object, and doesn’t care what code file it is or how it’s implemented internally.

We can solve this problem through the way of service. Simply speaking, I give you what you need. Through target-Action, the business provider provides all the services in the form of object methods, and calls and communicates between modules through the parameters and return values of the methods. As shown below:

2.3 Solve the dependency and decentralization among modules

After completing the first two steps, two questions remain:

  1. Mediator is a centralized service. Introducing Mediator will also introduce target-action of all business modules, making irrelevant services redundant. At the same time, after the service provided by all business modules is modified, it needs to be modified in Mediator. This makes Mediators increasingly difficult to maintain.
  2. The dependency between business modules is not reduced. Although only Mediator is imported when business calls, Mediator indirectly introduces target-Action, which in turn indirectly introduces business code files.

The solution to the first problem was to decentralize Mediator through the idea of composition, using Objective-C categories. Create a Mediator classification (Category) for each business module and introduce Target service into the Category, which is equivalent to encapsulating Target service with another layer of methods. Other business callers only need to introduce corresponding classification (Category). This avoids the unnecessary introduction of extraneous business services. At the same time, after the service provided by the business module was modified, the corresponding business provider only needed to modify its own Category, and mediators did not need to be maintained, thus achieving real decentralization. As shown below:

As you can see from the figure above, calls between modules are no longer introduced directly, but by Category. Based on the above, you can see that the dependency is not reduced. The Category refers to target-action and indirectly refers to the source file.

The second problem is the dependency between Category and target-action, and the solution is simple and crude. Because the implementation of the service Category in the business module is actually the Action method in the Target class that is called directly, the Runtime technology can directly cut off the dependency between the two.

  1. The Class object is retrieved from the NSClassFromString method and the Target Class name. The Class object is then retrieved from the Target instance by alloc and init methods.
  2. The SEL object is obtained using the NSSelectorFromString method and the Action method name.
  3. The Target instance object calls – (id)performSelector:(SEL)aSelector withObject:(id)object to complete the invocation and communication of the service.

By using the above method, a Category can call the target-action service without import and pass it out, thus completing the de-dependency. As shown below:

At this point, the business architecture design is clear. The above is the implementation plan of componentization engineering.

Componentized engineering implementation

3.1 Preparation for implementation

Create a new directory, ModularizationProject, to store all project implementation files. Then create a new directory, ConfigPods, under ModularizationProject, to store some configuration files, as shown in the following figure:

The templates directory contains files to help create Xcode projects, and the config.sh script allows you to quickly create projects and configure private Pods. See the sample

  • Gitignore filters files during Git commit.

  • Readme.md can provide descriptions of Git repositories, as needed.

  • Podfile is a necessary file to create the Cocoapods project. The first source address in the sample file is the remote Git repository address of the private Pods repository.

  • Podspec is the necessary configuration file to package projects into Pods, and the contents can be modified as needed. Using the sample file, it is recommended to modify only the information after S.thor.

  • Upload. Sh is packaged inside the pods orders, sample files inside push back is private pods, warehouse – sources at the back of the parameters of the first is the address of the remote Git repository private pods warehouse, need to own both of them.

  • Config. sh is the script for creating the entire project. You are advised to use the sample file without modifying it.

3.2 Creating a Service Module Project

  1. Create a remote repository of account modules in your Git organization.

  2. Create an iOS project called AccountModule in the ModularizationProject. Note that Source Control is unchecked during the creation.

  3. Open the terminal command line CD to the config.sh directory and run

    ./config.sh
    Copy the code

    Enter Project Name: Enter the Project Name

    AccountModule

    Enter HTTPS Repo URL: Enter the HTTPS address of the project’s remote Git repository

    Github.com/iOSShop/Acc…

    Enter SSH Repo URL: Enter the address of the project’s remote Git repository

    [email protected]:iOSShop/AccountModule.git

    Enter Home Page URL: Enter the project Home Page:

    Github.com/iOSShop/Acc…

    Confirm: Confirm the input information

    y

  4. The remote Git repository for the account module has been created and committed for the first initialization.

  5. Go to the local AccountModule directory and run

    pod install
    Copy the code

    Once complete, the Cocoapods project for the account module is created and ready for development. And the project with the qualification of the same directory, all code files in the directory can be.

  6. The preceding steps are used to create a service module project.

3.3 create Target – the Action

Before entering the development stage, responsibilities of each business module need to be divided and the external services provided by each business module should be well defined. Therefore, we can first complete the compilation of most target-actions and corresponding categories in the business module. Take the account module for example, other business modules may require a login page, a user’s login status, and changes to the user’s login status.

Method declarations in the account module target-Action:

#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface Target_Account : NSObject login / * * * * * / - (UIViewController *) Action_nativeLoginViewController; /** * Login status **/ - (BOOL)Action_nativeLoginStatus; / * * * * * / login status change - (nsstrings *) Action_nativeLoginStatusChangeNotificationName; @end NS_ASSUME_NONNULL_ENDCopy the code
  • The login page returns the corresponding INSTANCE of UIViewController.
  • Login status only needs to return a BOOL.
  • The change of login status is broadcast in the form of Notification, and other modules can realize the monitoring of login status by registering with the name of notification.

3.4 create a Category

The implementation of Mediator idea comes from CTMediator, and only two files at the core can satisfy most application scenarios. The framework can be directly relied on in actual project development, or it can be cloned and modified as needed. This is modified in the example to create a new CCMediator and make it into a private Pods library for use.

  1. Follow the full steps in 3.2 to create an AccountModule_Category project, then enter the AccountModule_Category project, edit the Podfile, add POD ‘CCMediator’, and then pod Install.

  2. Create a Category for CTMediator. Below are the declarations and implementations of the methods in the Category

    #import "CCMediator. H "NS_ASSUME_NONNULL_BEGIN @interface CCMediator (AccountModule) /** * presentViewController **/ - (UIViewController *)Account_viewControllerForLogin; /** * Login status **/ - (BOOL)Account_statusForLogin; / * * * * * / login status change - (nsstrings *) Account_nameForLoginStatusChangeNotification; @end NS_ASSUME_NONNULL_ENDCopy the code
    #import "CCMediator+AccountModule.h" NSString * const MediatorTargetAccount = @"Account"; NSString * const MediatorActionAccountLoginViewController = @"nativeLoginViewController"; NSString * const MediatorActionAccountLoginStatus = @"nativeLoginStatus"; NSString * const MediatorActionAccountLoginStatusChangeNotification = @"nativeLoginStatusChangeNotificationName"; @implementation CCMediator (AccountModule) /** * Logon (presentViewController) **/ - (UIViewController *)Account_viewControllerForLogin { UIViewController *viewController = [self performTarget:MediatorTargetAccount action:MediatorActionAccountLoginViewController params:nil shouldCacheTarget:NO]; if ([viewController isKindOfClass:[UIViewController class]]) { return viewController; } else { return [[UIViewController alloc] init]; }} / logged in * * * * * / - (BOOL) Account_statusForLogin {return [[self performTarget: MediatorTargetAccount action:MediatorActionAccountLoginStatus params:nil shouldCacheTarget:NO] boolValue]; } / * * * * * / login status change - (nsstrings *) Account_nameForLoginStatusChangeNotification {return [the self performTarget:MediatorTargetAccount action:MediatorActionAccountLoginStatusChangeNotification params:nil shouldCacheTarget:NO]; } @endCopy the code

    Service delivery was accomplished through Category, and the dependency between Category and target-Action was resolved in Mediator.

3.5 Making Private Pods

Once you’ve written the Category and target-action, you can commit to a remote repository via Git and generate Pods for reference by other business modules.

The steps are as follows:

  1. Edit the podSpec file, modify the version of S.Sion, and then customize it for resources and dependencies. See the official guide for details on how to use podSpec.

  2. Open the terminal CD to the project directory and start submitting the code.

    git add .
    git commit -m "add Target-Action"
    git push
    Copy the code
  3. Tag, make and send private Pods. Tag needs to be consistent with PODspec’s S.sion, and then execute the upload.sh script in the directory. An error may occur during execution. Follow instructions to resolve the error.

    Git tag 1.0.0 git push --tags./upload.shCopy the code
  4. You can see the pushed Pods information in both the local Pods repository and the remote Git repository.

    Here are the results of the pod Search:

  5. Other business modules can invoke AccountModule services by integrating AccountModule_Category and AccountModule directly into their project podfiles.

3.6 Service invocation between modules

We take the commodity module as an example. After entering the “My Commodity Interface”, we need to judge whether the user has logged in on this page. If the user has not logged in, it will prompt the user to log in and jump to the login page, and monitor the change of the user’s login status in real time.

  1. Monitor user status

    NSString *notificationName = [[CCMediator sharedInstance] Account_nameForLoginStatusChangeNotification];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loginStatusChange) name:notificationName object:nil];
    Copy the code
  2. Realize the method of monitoring, when the user logs in to hide the prompt landing page, when the user does not log in to display the prompt landing page.

    - (void)loginStatusChange { BOOL isLogin = [[CCMediator sharedInstance] Account_statusForLogin]; self.loginView.hidden = isLogin; if (isLogin) { [self.view bringSubviewToFront:self.loginView]; }}Copy the code
  3. The login page is displayed in response to the login prompt

    - (void)clickLogin {
        UIViewController *viewController = [[CCMediator sharedInstance] Account_viewControllerForLogin];
        [self presentViewController:[[UINavigationController alloc] initWithRootViewController:viewController] animated:YES completion:nil];
    }
    Copy the code

These are some basic service invocations that can be used for most intermodule invocation scenarios. Other types of services are left to their own devices, but it is important to note that the return value type of a method must be a primitive data type and a regular object. By normal objects, I mean objects in the Foundation framework, UIKit framework, or some other system library framework. If a custom object is used as the return value, the result will be strong coupling.

3.7 Service communication across modules

Communication is inevitable when different business modules are called, for example, jumping from the page of commodity details to the page of order generation. When the page of commodity details is called to the page of order generation, it needs to pass parameters including at least commodity ID and commodity quantity. The Category method declaration for order generation is as follows:

#import "CCMediator. H "NS_ASSUME_NONNULL_BEGIN @interface CCMediator (OrderModule) /** */ - (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount; @end NS_ASSUME_NONNULL_ENDCopy the code

All parameters passed by the Category are encapsulated in an NSDictonary, which is then passed to the corresponding target-action. The Category method is implemented as follows:

#import "CCMediator+OrderModule.h" NSString * const MediatorTargetOrder = @"Order"; NSString * const MediatorActionOrderMakeViewController = @"nativeOrderMakeViewController"; @implementation CCMediator (OrderModule) /** */ - (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount { if (goodsID == nil) { NSException * exception = [[NSException alloc] initWithName: @ "Order_viewControllerForMakeWithGoodsID: goodsCount: tips" Reason :@"goodsID cannot be null "userInfo:nil]; @throw exception; } if (goodsCount < 1) { NSException *exception = [[NSException alloc] InitWithName: @ "Order_viewControllerForMakeWithGoodsID: goodsCount: prompt" "reason: @" goodsCount error "the userInfo: nil]; @throw exception; } NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"goodsCount"] = [NSNumber numberWithInteger:goodsCount]; params[@"goodsID"] = goodsID; UIViewController *viewController = [self performTarget:MediatorTargetOrder action:MediatorActionOrderMakeViewController params:params shouldCacheTarget:NO]; if ([viewController isKindOfClass:[UIViewController class]]) { return viewController; } else { return [[UIViewController alloc] init]; } } @endCopy the code

If the parameter is necessary, it can be checked before being passed to target-action, and an exception can be thrown if it does not meet the requirement. Of course, it can also be customized according to the needs of the product. The corresponding target-action method declaration is:

#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface Target_Order : NSObject / * * * * * generate order / - (UIViewController *) Action_nativeOrderMakeViewControllerWithParams (params NSDictionary *); @end NS_ASSUME_NONNULL_ENDCopy the code

Method declarations with and without arguments have a WithParams. See the implementation of CCMediator. The corresponding target-action method is implemented as:

# # import "Target_Order. H" import "OrderMakeViewController. H" @ implementation Target_Order / * * * * * / - generated order (UIViewController *)Action_nativeOrderMakeViewControllerWithParams:(NSDictionary *)params { OrderMakeViewController *orderViewController = [[OrderMakeViewController alloc] init]; orderViewController.goodsCount = [params[@"goodsCount"] integerValue]; orderViewController.goodsID = params[@"goodsID"]; return orderViewController; } @endCopy the code

The reason why NSDictionary is used to pass parameters is because it is a container and belongs to a class in the Foundation framework. Using NSDictionary does not cause the dependency between Category and target-action. All parameters can be encapsulated and passed. Moreover, the parameter transfer between modules should be as little as possible, otherwise the coupling between modules will be enhanced. The parameters passed must also be basic data types and regular objects, not custom objects.

Invoke the order generation page from the item details page

- (void)clickBuy {
    UIViewController *viewController = [[CCMediator sharedInstance] Order_viewControllerForMakeWithGoodsID:self.goodsID goodsCount:99];
    [self.navigationController pushViewController:viewController animated:YES];
}
Copy the code

Communication across modules is described above, but the example is forward parameter passing. How to implement backward parameter passing? For example, in A common scenario, I go from page A to page B, and page B does something and passes some parameters to Page A. And the way to do that is to use blocks, encapsulate blocks into NSDictonary and pass them around. In the example scenario, the product details page enters the order generation page and returns the successful information to the product details page for display after payment is completed, as shown below:

The order module’s Category method declaration is now modified as follows:

#import "CCMediator.h" NS_ASSUME_NONNULL_BEGIN typedef void(^SuccessBlock)(NSString *); @ interface CCMediator (OrderModule) / * * * * * generate order / - Order_viewControllerForMakeWithGoodsID: (NSNumber (UIViewController *)  *)goodsID goodsCount:(NSInteger)goodsCount success:(SuccessBlock)successBlock; @end NS_ASSUME_NONNULL_ENDCopy the code

The implementation of Category method is modified as follows:

#import "CCMediator+OrderModule.h" NSString * const MediatorTargetOrder = @"Order"; NSString * const MediatorActionOrderMakeViewController = @"nativeOrderMakeViewController"; @implementation CCMediator (OrderModule) /** */ - (UIViewController *)Order_viewControllerForMakeWithGoodsID:(NSNumber *)goodsID goodsCount:(NSInteger)goodsCount success:(SuccessBlock)successBlock { if (goodsID == nil) { NSException *exception = [[NSException alloc] InitWithName: @ "Order_viewControllerForMakeWithGoodsID: goodsCount: prompt" "reason: @" goodsID cannot be empty. "" the userInfo: nil]; @throw exception; } if (goodsCount < 1) { NSException *exception = [[NSException alloc] InitWithName: @ "Order_viewControllerForMakeWithGoodsID: goodsCount: prompt" "reason: @" goodsCount error "the userInfo: nil]; @throw exception; } NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"goodsCount"] = [NSNumber numberWithInteger:goodsCount]; params[@"goodsID"] = goodsID; if (successBlock) { params[@"successBlock"] = successBlock; } UIViewController *viewController = [self performTarget:MediatorTargetOrder action:MediatorActionOrderMakeViewController params:params shouldCacheTarget:NO]; if ([viewController isKindOfClass:[UIViewController class]]) { return viewController; } else { return [[UIViewController alloc] init]; } } @endCopy the code

The target-Action implementation of the order module simply adds a line to the assignment:

orderViewController.successBlock = params[@"successBlock"];
Copy the code

The invocation of the product details page is modified as follows:

- (void)clickBuy {
    __weak __typeof(self)weakSelf = self;
    UIViewController *viewController = [[CCMediator sharedInstance] Order_viewControllerForMakeWithGoodsID:self.goodsID goodsCount:99 success:^(NSString * _Nonnull successString) {
        __strong __typeof(weakSelf)strongSelf = weakSelf;
        strongSelf.textLabel.text = successString;
    }];
    [self.navigationController pushViewController:viewController animated:YES];
}
Copy the code

Detailed implementation details can be found in the sample project.

3.8 Main engineering module integration

General applications are UITabBarController+UINavigationController, so our main project is basically to set up the structure of UITabBarController+UINavigationController, do some global Settings, And handle some initialization logic and so on. Then add all the business module’s Category projects and their corresponding business module projects to your Podfile.

3.9 Other Instructions

  1. In terms of inter-module calls and communication, the solution to dependencies also involves some hard coding, including the need to hardcode class and method names when calling and parameter names when passing parameters. All of this hard coding is unavoidable, but it is manageable, limited to Cateogry and its corresponding Target-Action. Therefore, Cateogry and target-Action of the same business module are basically written by the same person, which can guarantee no error.

  2. Dependency loops are important when writing podSpec files. Note:

    • A Podspec for a Category project should not be a dependency corresponding to a business project, nor a Category and its business project for another business module. Theoretically speaking, dependency only needs CCMediator.
    • A business project podSpec only needs a Category of another business module, not another business project.
    • You can add categories and business projects for other business modules to your Podfile during development tests.

    The reason for this is, for example, that the account module calls the services of the goods module, and the goods module calls the services of the account module. If the goods module’s Category project has a PodSpec that depends on the goods module’s business project, and the Accounts module’s Category project has a PodSpec that depends on the Accounts module’s business project. Then when the Category project of the account module is introduced into the business project of the goods module, the business project of the account module is introduced. Then the account module will introduce the Category project of the goods module, and the Category project of the goods module will introduce itself into the business project of the goods module, so it will not succeed in introducing itself. As shown below:

  3. Git will often fail to upload pods using the upload.sh command after the tag is completed. Once you resolve the error, you may need to rename the tag, causing the version number to jump. Therefore, you can delete the failed tag and re-type the tag.

    git tag -dGit push Origin :/refs/tags/1.0.0Copy the code

4 summarizes

This is the end of the componentization implementation. This article provides the basic ideas that are already sufficient for most business development scenarios. However, common network layer, common UI components and other parts are not covered. This is where you need to think about whether the functionality is business or non-business. If it is a business type, it is best to make it into a Category+ target-action mode to provide services. If there are non-business types, such as network requests, generic UI frameworks, etc., the authors suggest encapsulating these functions into a base module that can be made into components that all business modules can reference. Since the components of the base module are directly imported files, the base module should not have too many functions. Because once the module is too large, the dependencies and coupling are also greater. At the same time, as the scale of the project and the increase of business complexity, there will be more and more things to consider, which will test the ability of system architecture design. The practice of componentization needs to go step by step, and it also needs the continuous thinking, exploration and improvement of developers.

The above is the author in the actual development of the summary, please contact:

[email protected]

All the code for this article is hosted on Github.

The architectural design and practical ideas for this article are derived from the blog of Casa Taloyum:

  • IOS application architecture: Componentization solutions

  • Implement the componentization scheme based on CTMediator in existing projects