1. The opening

The original intention of this article is to give a suggestion and reference to students who are doing mixing or modularization.

Since the project I have done since I came to the factory is the only iOS project with Swift/OC mixed design in the whole company, so I have stepped into numerous pits along the way. Now I sum up the process and experience of some pits for your reference.

I believe that after browsing this article, there will be a harvest.

When I came, the project had already started Swift transformation, and the project was gradually Swift, and the new codes were all Swift.

The results of seven months will be announced first. The following is our final project structure:

For us to mix the situation, people began to discuss five months ago.

We are presented with two options:

  1. Slowly replace the OC code with Swift
  2. Modularize as quickly as possible and separate the two language code

We started with option 1, but it quickly became clear that for 74% of our projects that were OC code, it was too painful, too long, and the iterative process was constantly iterating and coupling.

So after some analysis of the stakes, we quickly jumped into option 2. On the one hand, modularity itself is the end result of increasingly bloated projects, on the other hand, it can be slowly stripped of the two languages.

Note: Modularization, also known as “componentization”, does not mean that modules are separated into folders in the main project. Rather, it means that individual modules are isolated into CocoaPods libraries or other forms of library files and become a single project.

2. Module division

How to cut the knife is the most important step of the modular mix, which completely determines whether the follow-up work is difficult or not.

Do not split from the business module, such as “real-time order module”, “history order module”, “personal center” so directly split, ensure that you cry to the back of their own.

The right thing to do is to start at the bottom, and the first things that come to mind are “class Extension Extension”, “utility class”, “network library”, and “DB management” (of course we don’t use the heavy DB for this).

What do we usually see when we see a large library or a company introduce their product architecture? Does it have to be OpenGL ES and Core Graphics on the bottom to have Core Animation on the top, and then UIKit on the bottom? The lower layer determines the upper layer. Only by pulling out the parts with high reuse rate can the upper layer services be gradually built.

Blog_ios-modullaser-04.png – 7534C8-1513047089367-0

So the first thing we did was to extract the utility class and Extension, such as:

  1. Various Constants files
  2. NSTimer,NSString,UILabelAnd so on
  3. RouterHelper,JavascripInterfaceAnd so on Utils and helpers

This piece of work, not only can extract the OC code, but also can extract the Swift code. We created a new library for the OC part of the code as LPDBOCFoundationgbage and a new library for the Swift part as LPDBPublicModule.

2.1 LPDBOCFoundationGarbage

First, LPD Bocfoundationgbage, which obviously doesn’t just put in the files mentioned above. Lpdbocfoundationgbage also loads OC code that doesn’t follow business changes over time. This is because, in practice, we always find that “the ideal is beautiful”, although everyone has the desire to sort out the old code, but in fact, the old code of our project has been cut to the point of chaos, so the idea of sorting out and separating is basically unreliable. This is the time to borrow a phrase that MM taught us: “Make nasty code nasty together.” LPDBocFoundationgbage was built for this purpose.

A large number of OC codes put in for long periods that do not follow business changes include:

  1. Custom Customer View, such as Refresh control, Loading control, red dot control, and so on
  2. Custom small controllers, such as TextField and its five or six filters PhoneNumValidator, IDCardValidator, etc
  3. Controllers that do not change with the business, such as customized AlertController, customized WebController, customized BaseViewController, and so on

At the end, our list of levels looks something like this:

A few words about prefixes. All of our extracted libraries have the prefix LPDB, but the Swift library and OC library are slightly different, OC library files also have prefix, Swift library is removed prefix, which also conforms to the specification of both languages.

2.2 LPDBPublicModule

The LPDBPublicModule situation is very simple, mainly some reusable code generated during the new business iteration, but this is obviously not the same as the OC garbage can, much cleaner. The main storage is:

  1. Swift Extension
  2. Lotusoot and other public protocols

Lotusoot is a modular tool and specification that I developed that I called “routing” at first, but then found that the department misinterpreted it by calling it “routing library,” so I ended up calling it “modular tool.” Check out this article on Lotusoot.

2.3 LPDBNetwork

This needless to say, no matter what items are some basic, basically network related old code of our project is OC, the only trouble is that our network layer, the early researchers write more rough, and even the UI layer code has a lot of coupling, such as network, according to the request and the network failure has some HUD Look around your chrysanthemums or something. So there was a lot of nausea when it was pulled away from the main project.

Therefore, for such strong coupling, the final solution is divided into two code transformation. In the first time, the OC code is extracted through reflection to ensure the availability of the code and pass the basic test. The second time is through protocol instead of the original reflection. The third time is useLotusootThoroughly regulate service invocation. This will be described in a later section, “Summary of some difficulties in the process”

2.4 LPDBUIKit

This is the Swift UI library, some of the more commonly used controls and so on.

2.5 LPDBEnvironment

This section is used for environment control to switch the server environment to be accessed. This section itself can not be taken out, but because it is dependent on other basic modules, such as LPDBNetwork, and there are a lot of related codes and environment-related codes are relatively independent, so it is taken out separately.

3. Remove the service module

At this point, the lower-level code is basically drawn out, and the rest can be extracted from the business library more easily.

The key points of extracting the business library are:

  1. Extracted business libraries are not changed frequently to prevent changes due to business requirements during extraction and refactoring
  2. Extracted business libraries can be highly independent and should be extracted like building blocks, for exampleLPDBLoginModule, extraction is quickly integrated into any module, and can ensure the login function, better service to other modules

The three business modules we have drawn out so far are LPDBHistoryModule, LPDBUserCenterModule, and LPDBLoginModule.

4. Some difficult points in the process

What’s left is to talk about some of the problems in this process.

4.1 Handling module coupling code – reflection calls

The main reason for using reflection the first time around is that usually when you recurse A file’s dependencies, you recurse A lot of things (especially our old code), often A->B->C->D->F, with various dependencies in between, and even references to Swift classes at the very last level. Until you finally get sick of #import. Give a picture to feel it:

Why isn’t there a way to solve coupling by protocol all at once?

This is mainly because using development mode for a single Pod library is easy to debug, but using development mode for two Pod libraries at the same time without releasing a version is more difficult to handle (see the section “Using private libraries” in this article). In this case, working with two or more libraries repeatedly is cumbersome, so the priority is to separate the code as quickly as possible and pass basic tests without compromising functionality.

So at the end of this process, there’s a bunch of NSClassFromStrings and so forth in the sublibrary.

Take LPDBLoginMoudle as an example:

NSString *className = [NSString stringWithFormat:@"%@.`AuthLoginManager"[NSString targetName]];
id authLoginManager = NSClassFromString(className);
if(! [authLoginManager conformsToProtocol:@protocol(authLoginSuccess)]) {
    return;
}
[authLoginManager authLoginSuccess];
Copy the code
id delegate = [[UIApplication sharedApplication] delegate];
[delegate jumpToShopListVC:shops];
Copy the code

4.2 Handling module coupling code – protocol calls

It is not advisable to keep the first pass full of NSClassFromStrings, because such code tends to be hard-coded and cannot throw an error at compile time if the class name or method name changes.

Here comes a discussion.

When I discussed the specific practices of modularization, I mentioned that the mainstream componentization probably uses the + (void)load and Rumtime operations to register routes and services. At this point, Casa puts forward a saying that “the fundamental purpose of componentization is isolation, isolation of problem domains, isolation of business, isolation of development dependency. If you don’t use runtime, you can save a lot of work. But using URL is a relatively redundant task. A string containing target-action is enough. At the same time, urls should be limited to H5 scheduling and CROSS-app URL Scheme scheduling.

I would like to make a very serious apology to Casa. The above paragraph was reserved for modification in the first edition. I intended to read “[iOS Application Architecture on Componenzation Scheme]” again after careful understanding and modification. There is a will to lead people to misinterpret the great God. Very, very sorry! It has now been modified. Here are the big guy’s own views on URL:

At that time, I heard casa’s statement and thought, “Hey? Sense “, but in the later practice, I think in my code, the hope as far as possible the problem exposed in the compile phase, can let it throws the error is thrown error, even if use the string can be defined constants, but because everyone is not responsible for the project independently, when others see you method parameters, such as: + (void)callService:(NSString *)sUrl or + (void)openURL:(NSString *)url, if the caller finds that your argument is NSStrring, it’s quite possible to get a hard-coded string without looking up the list of constants, This is a problem with habitual coding. However, I was quite satisfied with casA’s “URLS are less target-action represented”, so Lotusoot focused only on decouped service invocation. Urls are just to better provide external invocation services for H5 pages, which can be used in a more concise way within the project.

The last reason is that reflection or through a class/method string dictionary is too OC. Anyway, we are a Swift project and should try to take advantage of it. Although reflection can be used in the OC library, what about Swift library? Swift3 and swif4 currently do not support reflection well.

Therefore, it is necessary to replace reflection with protocol for the second processing. But essentially, it’s not handled very well. It looks like this (LPDBLoginModule as an example) :

4.2.1 Organize the services used in LPDBLoginModule and classify them

For example, our LPDBLoginModule uses some methods in the AppDelegate class and some methods in the AuthLogin class

4.2.2 Establish a protocol in LPDBLoginModule

Create AuthLogInDelegate. h and AppDelegateProtocol

The general code is as follows:

@protocol AppDelegateProtocol <NSObject>

- (void)jumpToHomeVC;
- (void)jumpToShopListVC:(NSArray *)shops;
- (CLLocationCoordinate2D)getCoordinate;

@end
Copy the code
@protocol AuthLoginDelegate <NSObject> [Pods] (media/Pods.).
+ (void)authLoginSuccess;
@end
Copy the code

4.2.3 Implement the agreement in the main project

AppDelegateProtocol is implemented by the AppDelegate extension:

@import LPDBLoginModule;
@interface AppDelegate (Protocol)  <AppDelegateProtocol>
@end

@implementation AppDelegate (Protocol)
- (CLLocationCoordinate2D)getCoordinate {
    ...
}
- (void)jumpToHomeVC {
    ...
}
- (void)jumpToShopListVC:(NSArray *)shops {
    ...
}
@end
Copy the code

AuthLoginDelegate is implemented by AuthLoginManager(which is written by Swift in the main project) :

extension AuthLoginManager: AuthLoginDelegate {
    static func authLoginSuccess(a) {
        .}}Copy the code

4.2.4 Invoking services on LPDBLoginModule

id delegate = [[UIApplication sharedApplication] delegate];

if(! [delegate conformsToProtocol:@protocol(AppDelegateProtocol)]) {
    return;
}
CLLocationCoordinate2D coordinate = [delegate coordinate];
Copy the code
NSString *className = [NSString stringWithFormat:@"%@.AuthLoginManager"[NSString targetName]];
id authLoginManager = NSClassFromString(className);
if(! [authLoginManager conformsToProtocol:@protocol(LPDBAuthLoginDelegate)]) {
     return;
}
[authLoginManager authLoginSuccess];
[self jumpToSelectShopView:shops];
Copy the code

After these modifications, the state between modules is shown in the figure below:

But there is a clear sense that the change is not radical:

  1. There are plenty of them! [delegate conformsToProtocol:@protocol(AppDelegateProtocol)]Such judgment only plays a fault tolerance role to ensure that crash will not occur, but it cannot expose the problem in the compilation stage.
  2. AppDelegateProtocolA common protocol used by multiple modules is definedLPDBLoginModule
  3. Reversing the concept, ideally each submodule should provide protocols and implementations that tell other modules what functions can be called from that module. Currently, submodules tell other modules which methods to call, which are implemented by other modules.

So to solve the problem once and for all, we introduced Lotusoot — Component communication and tools.

4.3 Handling module coupling code -Lotusoot

Lotusoot was originally designed to address module coupling and support both OC and Swift, which is one of the most important things I’ve done in the past few months. The library itself is small and flexible and contains very little, but I’m very pleased with its specification.

The core ideas of the Lotusoot specification are the following steps, again using the above LPDBLoginModule as an example:

4.3.1 Creating a Shared Module — LPDBPublicModule

LPDBPublicModule defines the services that each module can provide in a protocol called Lotus. A Lotus protocol contains a list of all the methods that a module can call. Examples are as follows:

@objc public protocol AppDelegateLotus {
    func jumpToHomeVC(a)
    func jumpToSelectShopVC(shops: [Any].isNapos: Bool)
    func getCoordinate(a) -> CLLocationCoordinate2D
}
Copy the code
@objc public protocol MainLotus {
    func authLoginSuccess(a)
}
Copy the code

4.3.2 In each module, the corresponding Lotus protocol in LPDBPublicModule is implemented

The Class that implements the protocol is called Lotusoot. Examples are as follows:

class AppDelegateLotusoot: NSObject.AppDelegateLotus {

    func jumpToHomeVC(a) {
        .
    }
    
    func jumpToSelectShopVC(shops: [Any].isNapos: Bool) {
        .
    }

    func getCoordinate(a) -> CLLocationCoordinate2D {
        .}}Copy the code
class MainLotusoot: NSObject.MainLotus {
    func authLoginSuccess(a) {
        .}}Copy the code

4.3.3 Registration service

It is important to note that this step can be omitted and all routes can be automatically registered with the scripts and annotations provided with Lotusoot. Please clickLotusootSee section “3. Notes and Specifications”.

The registration service didFinishLaunchingWithOptions:

[LotusootCoordinator registerWithLotusoot:[AppDelegateLotusoot new] lotusName:@"AppDelegateLotus"];
    [LotusootCoordinator registerWithLotusoot:[MainLotusoot new] lotusName:@"MainLotus"];
Copy the code

4.3.3 Invoking services in other modules

Now you just need to import Lotusoot, Import ModulePublic

id<MainLotus> mainModule = [LotusootCoordinator lotusootWithLotus:@"MainLotus"];
[mainModule authLoginSuccess];
Copy the code
// If the string @"AppDelegateLotus" is registered, it is recommended to define it in LPDBPublicModule
// You can also use NSStirngFromClass(AppDelegatelotus.class)
id<AppDelegateLotus> appDelegateLotus = [LotusootCoordinator lotusootWithLotus:@"AppDelegateLotus"];
[appDelegateLotus goToHomeVC];
Copy the code

Both OC and Swift can be called smoothly

Or use a string like "AccountLotus", but you need to manage kAccountLotus and try not to hardcode it
let appDelegateLotus = s(AppDelegateLotus.self) 
let appDelegateLotusoot: AppDelegateLotus = LotusootCoordinator.lotusoot(lotus: appDelegateLotus) as! AppDelegateLotus
accountModule.goToHomeVC()
Copy the code
let mainLotus = s(MainLotus.self) 
let mainModule: MainLotus = LotusootCoordinator.lotusoot(lotus: mainLotus) as! MainLotus
mainModule.authLoginSuccess()
Copy the code

So far, the coupling between modules is relatively complete. The clean style is represented by a graphic (which I used in my Lotusoot commentary) :

The Lotus protocol in the LPDBPublicModule lists the service declarations provided by all modules, and within each module, the desired service can be invoked directly through these common protocols. Many problems can be shown before and during compilation (if the module does not provide services, it cannot be compiled; If no service is declared, it will not compile.

4.4 Language Coupling

An important purpose of our module extraction is to “split the two languages”, but in practice, we will find that language segmentation is more difficult than business segmentation.

Only one language can be contained in a Pod library, but often, at the end of the code abstraction, there are countless underlying Model couplings, such as:

@interface ShopInfo : LPDBModel.@property (nullable.nonatomic.strong) DeliveryService *workingProduct;
@property (nullable.nonatomic.strong) DeliveryService *preEffectiveProduct;

@end
Copy the code
class DeliveryService: BaseModel {
    .
}
Copy the code

If you need to extract ShopInfo and DeliveryService into a module, you must “give and take”, which can be appropriately rewritten when the basic Model language is different, because the Model code is very small, and the Model usually only contains attribute declarations. As an intermediary of data transfer, even if changes are made, the likelihood of unprefundable errors is lower.

If the module body to be extracted uses OC, then DeliveryService can be re-written in OC.

However, it should be noted that first try to split the more basic service modules, before considering rewriting the file, to ensure the stability of the project.

4.5 Building blocks of modules

The ultimate goal of modularization is not only decoupling, but also to make each module like building blocks, splicing at will. Finally, there is no code in the main project, and each module is integrated through Pod to form a complete function. Each module should be able to be tested and developed independently.

Again, take LPDBLoginModule and LPDBNetWort as examples.

Login module is a very special module. If all sub-modules want to test and develop independently, they generally need to pass login verification. For example, the order module must be logged in so that the order information can be correctly pulled from the business module.

Since LPDBLoginModule relies on the base library LPDBNetWort, LPDBNetWort needs to do the following:

  1. Contains a CER file, which can be correctly provided to other modules for normal HTTPS interface access
  2. Convenient network service invocation

The LPDBLoginModule should at least do the following:

  1. The login information can be saved correctly
  2. Provides the login UI interface, you can directly call LoginVC

After having the above functions, LPDBLoginModule can be quickly integrated into other modules to provide independent development and testing functions for other modules.

4.6 Resource Packaging

The previous summary mentioned that the LPDBLoginModule should provide a LOGIN UI. For the UI interface, you need to do resource packaging, and in module splitting, you need to be very careful about resource splitting.

Because of the division of business modules, there is not only code extraction, but also resource extraction.

The resource library includes but is not limited to:

  1. .xibfile
  2. Voice resources
  3. Image resources
  4. Plain text file
  5. Video resources

Therefore, all resource files should be placed in a separate Res folder with the resource file path indicated in.podSpec

s.resources 	 = ["Source/**/*.xib", "Source/Res/*.xcassets"]
Copy the code

Note the image resources, if you want to keep @2x, @3x, you can copy them directly according to the format of Xcassets.

At the end of may

The above is my experience summary of modularization/componentization in the mixed programming project, written as a guide mode, I hope this article can be helpful to people who take the same road, I hope you will gain something.