Before because of

Our seven-person iOS development team wasn’t suited for componentized development. The reason is that the cost is low, it takes a lot of time and experience to do it, and the revenue doesn’t completely change anything. However, I was not very busy because I had a gap of two or three weeks. Another is that you can use it in a brand new App. So I decided to try componentized development.

To try is to try to solve the problems of componentized development. If you can solve it, and there is a good solution, then move on, otherwise quit.

background

It is unreasonable to talk about the selection of the scheme from the actual situation.

So a little background: We are a nasdaq-listed technology company. We also have several apps that are maintained by different teams, and we are one of them. Our team is a small iOS development team of seven people. The author himself is the group leader.

Previous apps have been developed using CocoaPods and have used a binary scheme. Apps already use automated integration.

Although a new App is to be developed, many businesses are the same or similar to the previous App.

Why write this blog?

Wanted to document the whole process for later review.

Our ideas and solutions may not be right or the best. So hopefully, after reading this blog post, you can provide us with suggestions and other solutions to optimize and make this componentized development solution even better.

Technology stack

  • gitlab
  • gitlab-runner
  • CocoaPods
  • CocoaPods-Packager
  • fir
  • Binary,
  • fastlane
  • deploymate
  • oclint
  • Kiwi

results

After using componentized App development:

  • Code submission is more standardized and quality is improved. The number of bugs reported by testers decreased significantly.
  • Compilation speeds up. In the case of all source code: the entire compilation of the original App takes about 150s before developers can start debugging. Now, after componentization, a service component only needs about 10 to 20 seconds. Business components typically compile less than 10s when relying on binary components.
  • The division of labor is more clear, thus improving the development efficiency.
  • Flexible, low coupling.
  • Combining with the MVVM. Very detailed unit tests to improve code quality and ensure App stability. The number of bugs reported by testers decreased significantly.
  • Rollback is more convenient. It’s not uncommon for our business or UI to go back to the previous version, where we used to checkout out the previous code. Now with componentization, we just need to use the old version of the business component Pod library, or send another Pod library based on the old version.
  • It’s easier for newcomers to get started.

For me:

  • Easier control of code quality.
  • It’s easier to keep track of what team members have done.
  • Assign work more easily.
  • Make it easier to arrange new members.

The decoupling

The idea is that even if we don’t end up doing componential development, it doesn’t matter if we pull out the reusable code and make it into a Pod library. That’s why I made it a priority.

What should be a Pod library?

Our previous apps have been CocoaPods. We’ve pulled things like Util, Category, network layer, local storage, etc. into a Pod library that will be reused across apps. There are also some business related ones, such as YTXChart,YTXChartSocket; These are also reused across apps.

So the simple conclusion is that code to be shared between apps should be split into Pod libraries and treated as individual components.

We went and looked at the original App code, and there were a lot of things that needed to be reused that we didn’t componentalize.

Why isn’t this code componentized?

Because I didn’t think out how to decouple, for example.

There is a class called YTXAnalytics. UMengAnalytics is used to do statistics. Its coupling lies in a method. This method is used to gather information. It relies on User, and it relies on this currentServerId thing.

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__+ (NSDictionary*)collectEventInfo:(NSString*)event withData:(NSDictionary*)data
{
.......
    return @{
        @"event" : event,
        @"eventType" : @"event",
        @"time" : [[[NSDate date] timeIntervalSince1970InMillionSecond] stringValue],
        @"os" : device.systemName,
        @"osVersion" : device.systemVersion,
        @"device" : device.model,
        @"screen" : screenStr,
        @"network" : [YTXAnalytics networkType],
        @"appVersion" : [AppInfo appVersion],
        @"channel" : [AppInfo marketId],
        @"deviceId" : [ASIdentifierManager sharedManager].advertisingIdentifier.UUIDString,
        @"username" : objectOrNull([YTXUserManager sharedManager].currentUser.username),
        @"userType" : objectOrNull([[YTXUserManager sharedManager].currentUser.userType stringValue]),
        @"company" : [[ServiceProvider sharedServiceProvider].currentServerId stringValue],
        @"ip" : objectOrNull([SSNetworkInfo currentIPAddress]),
        @"data" : jsonStr
    };
}__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

The solution is to create a block that takes the responsibility for getting this information out.

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__ [YTXAnalytics sharedAnalytics].analyticsDataBlock = ^ NSDictionary *() { return @{ @"appVersion" : objectOrNull([PBBasicProviderModule appVersion]), @"channel" : objectOrNull([PBBasicProviderModule marketId]), @"username" : objectOrNull([PBUserManager shared].currentUser.username), @"userType" : objectOrNull([PBUserManager shared].currentUser.userType), @"company" : objectOrNull([PBUserManager shared].currentUser.serverId), @"ip" : objectOrNull([SSNetworkInfo currentIPAddress]) }; }; __Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

Most of our coupling is this way. The solution is to create a block that takes the responsibility of getting information out.

We can decouple them in the following ways:

  1. Make the code it depends on a Pod library and then rely on it instead. It’s kind of like “sink dependent.”

  2. Change dependencies to combinations using categories.

  3. Use a block or delegate (protocol) to throw this responsibility away.

  4. Copy code directly. Copy code may seem like an inelegant thing to do, but it has the advantage of being fast. For some unimportant tool methods, you can also directly copy to internal use.

Initialize the

The AppDelegate is full of initializations. Like our own code. It’s only a partial intercept!

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__ [self setupScreenShowManager]; //event start [YTXAnalytics createYtxanalyticsTable]; [YTXAnalytics start]; [YTXAnalytics page:APP_OPEN]; [YTXAnalytics sharedAnalytics].analyticsDataBlock = ^ NSDictionary *() { return @{ @"appVersion" : objectOrNull([AppInfo appVersion]), ....... @"ip" : objectOrNull([SSNetworkInfo currentIPAddress]), }; }; [self registerPreloadConfig]; / / Migrate standardUserDefault UserDefault transfer to group [NSUserDefaults migrateOldUserDefaultToGroup]; [ServiceProvider sharedServiceProvider]; [YTXChatManager sharedYTXChatManager]; [ChartSocketManager sharedChartSocketController].delegate = [ChartProvider sharedChartProvider]; // Initialize the initial set of quotes [[ChartProvider sharedChartProvider] addMetalList:[ChartSocketManager sharedChartSocketController].quoteList]; // Initialize ring message Manager [YTXEaseMobManager sharedManager]; __Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

Like third parties:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__ // Register ring letter [self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions]; //Talking Data [self setupTalkingData]; [self setupAdTalkingData]; [self setupShareSDK]; [self setupUmeng]; [self setupJSPatch]; [self setupAdhocSDK]; [YTXGdtAnalytics communicateWithGdt]; __Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

First of all, these initializations are used by various business components.

So when I componentize development, how does each business component make sure that I’m initialized when I use these things? Is every business component initialized once? What if I have parameters? Can I use singletons?

But the problem is that the third party library basically needs to register an AppKey, I write one for each business component? That’s not good, so I’ll configure it in the info.plist in the main App and initialize every business component. There won’t be any side effects. But it doesn’t feel elegant, and there’s a lot of repetitive code. If an AppKey or important parameter changes, then every business component has to change. That’s not going to work. On the other hand, my business components must depend on the contents of the main App. Whether it’s debugging in the main App or copying info.plist from the main App.

More key is there are some third-party libraries need in application: didFinishLaunchingWithOptions: initialization.

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__ ShareSDK, Allies, Talking Data such as [self setupThirdParty: application didFinishLaunchingWithOptions: launchOptions]; __Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

Is there a better way?

First I wrote a YTXModule. It makes use of the Runtime to capture the App life cycle without adding any code to the AppDelegate.

Used in.m of a class that wants to get the App lifecycle:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__YTXMODULE_EXTERN() {// equivalent to load isLoad = YES; } + (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions {// Implement the same method name, but it must be static. return YES; }__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

layered

This is because the hierarchy needs to be designed before the initialization problem is solved. So there’s a sudden jump to stratification.

On a graph:

We set ourselves a few principles.

  • There can be no dependencies between business components.
  • Do not cross – layer dependencies as illustrated.
  • Weak business components are code that contains a small portion of the business and can be reused across business components within the App.
  • A component that depends on YTXModule must end in Module, and it must be a business component or weak business component.
  • Weak business components start with an App code (such as PB) and end with Module. Example: PBBasicProviderModule.
  • Business components start with an App code (such as PB) and end with a BusinessModule. Example: PBHomePageBusinessModule.

It is an accepted principle that business components cannot have dependencies between them. Otherwise, the core value of componentized development is lost.

There should also be no dependencies between weak business components. If there are dependencies, you’re not dividing the function properly.

Initialize the

Once we’ve agreed on the hierarchy, we’ve defined the responsibilities. We can jump back to the initialization design.

Create a PBBasicProviderModule weak business component.

  • It relies on YTXModule to capture the App life cycle.
  • It’s responsible for initializing its own and third party stuff.
  • All business components can depend on this weak business component.
  • It makes sure that everything is initialized.
  • It’s to unify management.
  • It exposes classes and functionality for business components to use.

It is the PBBasicProviderModule that the business component relies on to ensure that everything in it is usable.

The PBBasicProviderModule makes me more aware of the concept of weak business components.

Because we’re lazy, if you define PBBasicProvider as a business component. Communication between it and other business components must be via Bus, Notification, protocol, etc.

But it must be business. Because those Appkeys must be related to the App, that is, the relevant configuration and parameters of the App can also be said to be business; I need to initialize those blocks that depend on User information, CurrentServerId, etc. It must be business.

We’ll have to start a weak business. Because I can’t break this rule: business components can’t depend on each other.

Further distinguish between weak business components and business components.

There are basically all business components: storyboards, NIBs, graphics, and so on. Weak business components are generally absent. It’s not absolute, but it’s the general case.

The business component is usually a specific business on the App. Such as home page, I, live, market details, XX trading market, YY trading market, XX trading medium, information, discovery and so on. Weak business components provide functions for these business components and are not directly displayed on the App.

We can also create weak business components to provide functionality to business components. Of course, it can’t be abused. Responsibilities need to be accurately delineated.

Finally, the code looks something like this:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__@implementation PBBasicProviderModule YTXMODULE_EXTERN() { } + (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions { [self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions]; [self setupBasic:application didFinishLaunchingWithOptions:launchOptions]; return YES; } + (void) setupThirdParty:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions]; [self setupTalkingData]; [self setupAdTalkingData]; [self setupShareSDK]; [self setupJSPatch]; [self setupUmeng]; // [self setupAdhoc]; }); } + (void) setupBasic:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self registerBasic]; [self autoIncrementOpenAppCount]; [self setupScreenShowManager]; [self setupYTXAnalytics]; [self setupRemoteHook]; } + (YTXAnalytics) sharedYTXAnalytics { return ...... ; }... __Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

imagine

The PBBasicProviderModule is a hodgepodge of things that were previously written in the AppDelegate. There is no grace.

It’s true. It feels like there’s no better way.

Now that it is. Can we just assume that every developer developing his or her own business component doesn’t need to care about the main App?

Because I know meituan’s componentized development has to rely on a bunch of Settings and initializations from the main App’s AppDelegate. So they just integrated debugging directly into the main App, and they made the main App build very fast by binalizing and removing Pod dependencies.

So can we continue to contaminate the PBBasicProviderModule? Don’t need to write any initialization code in the App delegate in the main App project? Basically, or as little as possible, writing any code in the main App? Instead of relying on the main App to rely on this weak business component?

Following this idea we empty out all the code in the AppDelegate. Things like initializing App style, initializing RootViewController, etc., can all be moved into a new weak business component.

The business component doesn’t need to care about the weak business component at all. All the developer needs to do is initialize his business component’s RootViewController in the AppDelegate of the Example App in the business component.

Leave the rest to this new weak business component. The main App and Example App just rely on it in your Podfile.

So the final idea is: developers don’t change the main App project, and they don’t need to know about the main App project. For developers, there is a disconnect between the main App and business components.

There is an even bigger benefit, I can just replace the weak business component and the business component can be adapted to a new App immediately. This is also a kind of decoupling.

Debug/Release

Who says you don’t have to write any code in the main App’s AppDelegate?

In our testing of binary Pod libraries, we found that source code passed, binary (.a) did not. Scratching my head, I then took a closer look at the code and found that it was this macro pot:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__#ifdef DEBUG

#endif__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

DEBUG is determined at compile time. It’s already compiled when you binary it. Our code is full of #ifdef DEBUG and so on and so forth. So what? This is a binary pot. But our binarization has become a standard, and we all consciously do it. How do we solve this problem?

The solution is:

A PBEnvironmentProvider is created. Everybody depends on it.

Then the original code for judging macros is changed to look like this:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__if([PBEnvironmentProvider testing])
{
//...
}__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

In the main App’s AppDelegate:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__#if DEBUG && TESTING // The PBEnvironmentProvider provided macro CONFIG_ENVIRONMENT_TESTING #endif__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

The principle is: if the AppDelegate has a method (CONFIG_ENVIRONMENT_TESTING macro provides this method), [PBEnvironmentProvider testing] gets YES.

Why put it in the main App? You can also drop it in the PBBasicProviderModule to provide a method.

Because the main App appdelegate. m is the source, not compiled. Also notice the TESTING macro. We can add a macro TESTING parameter to xcode and change it to 0 to generate an App that is actually DEBUG but contains online content.

This requirement comes from the fact that we often need to urgently build an app directly to the phone via Xcode to solve or confirm online problems.

Although hit in the face, but also ok, need not change in the future. And this is a special need. There’s no code for the main App other than this.

Communication between business components

We solved the problem of initialization and decoupling. The next step is to solve the problem of communication between components.

Then I found several third-party libraries and chose MGJRouter. I could have relied on it directly.

Later I realized that using all blocks resulted in code that was all stacked in one method:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__+ (void) setupRouter { ...... [MGJRouter registerURLPattern:@"mgj://foo/a" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]); }]; [MGJRouter registerURLPattern:@"mgj://foo/b" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]); }]; . }__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

It feels bad. So I just copied the MGJRouter code, and I changed the Block to @selector. And add it directly to the YTXModule. And use macros to make the results look elegant. The code looks like this:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__ YTXMODULE_EXTERN_ROUTER_OBJECT_METHOD(@"object1") {YTXMODULE_EXAPAND_PARAMETERS(parameters) NSLog(@"%@ %@", userInfo, completion); isCallRouterObjectMacro2 = YES; Return @" I am a type "; } YTXMODULE_EXTERN_ROUTER_METHOD(@"YTX://QUERY/:query") { YTXMODULE_EXAPAND_PARAMETERS(parameters) NSLog(@"%@ %@", userInfo, completion); testQueryStringQueryValue = parameters[@"query"];; testQueryStringNameValue = parameters[@"name"]; testQueryStringAgeValue = parameters[@"age"]; }__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

When called, it looks like this:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__ [YTXModule openURL:@"YTX://QUERY/query?age=18&name=CJ" withUserInfo:@{@"Test":@1} completion:nil]; NSString * testObject2 = [YTXModule objectForURL:@"object1" withUserInfo:@{@"Test":@2}]; __Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

The communication problem was solved. In fact, the page jump problem has been solved.

Page jump

The page-skipping solution is the same as the communication problem between business components.

However, it is important to note that you should also use the URL+Router to jump to a page within a business component, rather than pushing the ViewController directly.

The advantage is that you don’t need to register a URL if some internal jump page needs to be called by other business components in the future. Because there is.

Whether to de-model

Demodeling is mainly about communication between business components, whether to pass a Model (one of the keys in the passed Dictionary is Model).

If de-modeled, how does the developer of the business component determine what is in the Dictionary and what type? There needs to be a place to disseminate this information, such as in a header file, wiki, etc.

If you do not modelize, you need to make the Model into a Pod library. Both business components depend on it.

Finally decided not to go to Model. Because there are actually some models that are common between business components (such as users), there are bound to be Pod libraries. We can make it a remodel with network requests and local storage methods. The only inevitable problem is that developers of both business components might change the Model’s Pod library.

Disclosure of information

What parameters should be passed to the page? What is the essential vehicle for transferring data between business components?

How do different business developers know about this information?

With and without de-modeling, we all have our own solutions.

To de-model, expose the header file and write detailed comments in the header file.

If you don’t want to de-model, just look at the Model. In special cases, documents are written in header files.

In conclusion: The way to disclose information is to write comment documents in header files.

Component life cycle

Business components have the same life cycle as apps. It is a class itself, exposing only class methods and requiring no instances, so there is no such thing as a life cycle. And it can create a bunch of viewControllers using class methods, and the lifecycle of the ViewController is managed by the App. Even if these viewControllers need to communicate with each other, you can use Bus/YTXModule/ protocol to do so instead of letting the business component class take care of the communication between them; You shouldn’t own a ViewController; This increases coupling.

The life cycle of a weak business component is managed by the object that created it. Create on demand and ARC automatically release.

The life cycle of the underlying functional component and third party is managed by the object that created it. Create on demand and ARC automatically release.

Version of the specification

We made the rules ourselves.

All Pod libraries rely only on Minor

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__"~> 2.3"__Mon Oct 09 2017 15:13:27  GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

Patch is precisely depended on in the main App

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__"2.3.1"__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

The main. Minor of the business component version in the Main App must be the same as the Main App version.

Reference: Semantic Versioning RubyGems Versioning Policies

Binary,

Binarization I think is necessary to speed up development.

And this binary scheme that I’m using

The catch is that on gitlab-runner, when switching between binary and source code, it often requires pod cache clean –all, test/lint/publish to succeed. Every time pod cache is clean –all, CocoaPods will download the relevant POD library again, adding time and unnecessary overhead.

We have now solved the cache problem by adding preserve_paths to our PodSpec and executing download_zip.sh. The principle is to make pod cache both source code and binary. See name.podspec and download_zip.sh in the ytx-Pod-template project for details.

Binaries also have macro issues to worry about. Be careful with macros, especially #ifdef. Avoid discrepancies between source code and binary code.

Integrated debugging

Integration debugging is simple. Each business component is debugged in its own Example App.

The Business component’s PodSpec just needs to know what libraries it depends on. The rest of the business components should be in the Example App’s Podfile.

The dependent Pod libraries are binary. IS_SOURCE=1 pod install

Developers really only need to care about their own business component, which is self-consistent.

The question of who will maintain the public library

This problem does not exist in our small Team. I haven’t really thought about it. However, as long as Code approval (Test/Lint/Code Review) and permissions are managed properly, there should be no major problems.

Unit testing

We used Kiwi for unit tests. The ViewModel of each business component was unit tested in conjunction with the MVVM pattern. Gitlab-runner automatically runs tests every time the code is pushed. Developers can find problems as soon as they discover that a test has failed. It is also easy to trace which submission failed the test run.

This is also mandatory for our team. Merge Request was rejected without tests, tests were not written well, tests failed.

lint

Republishing lint for each component ensures correctness. This is also a mandatory step.

Lint can find a lot of problems. Warning is not normally allowed. If you cannot avoid (such as a third party) use –allow-warnings.

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__pod lib lint --sources=$SOURCES --verbose --fail-fast --use-libraries__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

Unified network service and local storage

This one is pretty easy. It would be nice to abstract these two parts into several Pod libraries for all business components to use. Here are three Pod libraries:

  • YTXRequest
  • YTXRestfulModel
  • NSUserDefault+YTX

Some other things

Ignore podfile. lock in the main App to avoid collisions.

Use source code for main App Archive, not binary.

Later check the code using Oclint and deploymate.

Use Fastlane Match to maintain development certificates.

For Pod library modules that need to read configurations from PList or JSON, add a namespace. A namespace can be the name of the business component.

The difference between business components reading resource files

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__# If the image wants to be found in a storyboard, use this method. S.resource = ["#{s.name}/Assets/**"] # Plist that you want to use within the bundle of my business component. As a configuration file. This is the official recommendation. s.resource_bundles = { "{s.name}/" => ["{s.name}/Assets/config.plist"] }__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon  Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

Continuous integration

The original App was continuous integration. Naturally, we expect new componentized apps to be continuously integrated as well.

The Podfile should look like this: All that appears in it are private Pod libraries.

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__pod 'YTXRequest', '2.0.1' pod 'YTXUtilCategory', '1.6.0 pod' PBBasicProviderModule ', '0.2.1 pod' PBBasicChartAndSocketModule ', '0.3.1' pod 'PBBasicAppInitModule', '0.5.1'... Pod 'PBBasicHomepageBusinessModule', '1.2.15 pod' PBBasicMeBusinessModule ', '1.2.10 pod' PBBasicLiveBusinessModule ', '1.2.1' pod 'PBBasicChartBusinessModule', '1.2.6 pod' PBBasicTradeBusinessModule ', '1.2.7'... __Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

If the Pod relies on a very, very large number of things, say 100 or more. In addition, you must rely on the main App for integration debugging. You can also expand all of your Pod library dependencies and write them to your main App’s Podfile. The Pod library is released without any dependencies in the PodSpec. This avoids the time-consuming problem of resolving dependencies during Pod Install.

Each script is in this ytx-pod-template. Let’s start with.gitlab-ci.yml.

Our continuous integration tool is GitLab Runner.

The whole process of continuous integration is:

The first step:

Create a Pod using template. Like this:

__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__pod lib create <Pod library name > --template-url="http://gitlab.baidao.com/pods/ytx-pod-template"__Mon Oct 09 2017 15:13:27 GMT+0800 (CST)____Mon Oct 09 2017 15:13:27 GMT+0800 (CST)__Copy the code

The second step:

Create the dev branch. For development.

Step 3:

Every time a push dev triggers the runner to automatically run Stage: Init Lint(test)

Step 4:

1. Prepare to publish the Pod library. Modify the version number of your PodSpec and tag it accordingly. 2. Use merge_request.sh to submit a merge Request to the master.

Step 5:

1. Merge Request is accepted after code review by other developers with permission. 3. Master triggers runner to automatically run Stage: Init Package Lint ReleasePod UpdateApp

Step 6:

If step five is correct. The dev branch of the main App receives a Merge Request to modify the Podfile. The reason why AFNetworking appears in the picture is that we are testing at this time.

Step 7:

The main App triggers runner, which will build an IPA and upload it to fir automatically.

Init

  • Initialize some environments.
  • Print out some information.

Package

  • Binary and package it into.A

Lint

  • Pod lib lint. Both binary and source code are lint.
  • The test.
  • Consider joining Oclint and Deploymate in the future.

ReleasePod

  • Zip the related files and upload them to the static server library. To provide binary download packages.
  • Pod repo push. Publish the Pod library.

ReleasePod library warnings will not be allowed.

UpdateApp

  • Download the App code
  • Modify the Podfile file. If it matches the POD library file name, modify it, otherwise add it.
  • Generate a Merge Request to the dev branch of the main App.

About GitLab Runner.

The stage feature is awesome. Highly recommended.

Each stage can run on a different runner. If each stage fails, it can be retry separately. Tasks within a stage can be executed in parallel :(test and lint are in parallel)


Thanks to Xu Chuan for correcting this article.

To contribute or translate InfoQ Chinese, please email [email protected]. You are also welcome to follow us on Sina Weibo (@InfoQ, @Ding Xiaoyun) and wechat (wechat id: InfoQChina).