directory

  • So modularity
  • How to separate the middle layer from the business layer
  • The similarities and differences between performSelector and protocol
  • Call way
  • Routing policy of middleware
  • Module entry
  • Compatibility with lower versions
  • Redirected routing
  • Project structure
  • Degree of modularity
  • Which modules are suitable for sinking
  • About Collaborative Development
  • Results demonstrate

So modularity

There are many articles about modularity on the web, and here’s an IOS- Componentized Architecture Talk for those interested.

So there are three stages

In MVC mode, our total project looks like this:

Add an intermediate layer that calls the specified file

Decouple the middle tier from the module


How to separate the middle layer from the business layer

  • The basic principle in the second picture is:

Coupling code that was originally in the business file (KTHomeViewController) code

KTAModuleUserViewController * vc = [[KTAModuleUserViewController alloc]initWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];Copy the code

Move to the intermediate layer (KTComponentManager)

//KTHomeViewController.h  

UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];

//KTComponentManager.h
return [[KTAModuleUserViewController alloc]initWithUserName:userName age:age];Copy the code

It appears that businesses are decoupled from each other, but the middle tier will reference all business modules. It simply transfers the coupled object.

  • Decoupled

You want decoupling without referring to header files. Instead of a reference to a header file, use a string. In a nutshell, there are two ways:

1. – (id)performSelector:(SEL)aSelector withObject:(id)object;

In specific use

Class targetClass = NSClassFromString(@"targetName");
SEL action = NSSelectorFromString(@"ActionName");
return [target performSelector:action withObject:params];Copy the code

The problem with this, however, is that if the return value is not of type ID, it may cause a crash. This can be compensated by NSInvocation, however. This code is from iOS Building a Componentized Project Architecture from Zero to one.

- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    const char* retType = [methodSig methodReturnType];

    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil;
    }

    if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        BOOL result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        CGFloat result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}Copy the code
  1. Call unknown object methods using protocol (which is how I use it)

First you need an agreement:

@protocol KTComponentManagerProtocol <NSObject>

+ (id)handleAction:(NSString *)action params:(NSDictionary *)params;

@endCopy the code

Then call:

ReturnObj = [targetClass respondsToSelector:@selector(handleAction:params:)]) {// Send Action messages to the registered object handleAction:actionName params:params]; }else {// Unregistered, further processing. NSLog(@" unregistered method "); }Copy the code

If there is a return base type, it can be handled in the specific entry file:

+ (id)handleAction:(NSString *)action params:(NSDictionary *)params {
    id returnValue = nil;

    if ([action isEqualToString:@"isLogin"]) {
        returnValue = @([[KTLoginManager sharedInstance] isLogin]);
    }
    if ([action isEqualToString:@"loginIfNeed"]) {
        returnValue = @([[KTLoginManager sharedInstance] loginIfNeed]);
    }
    
    if ([action isEqualToString:@"loginOut"]) {
        [[KTLoginManager sharedInstance] loginOut];
    }
    return returnValue;
}Copy the code

The similarities and differences between performSelector and protocol

The two approaches share the same central idea and have a lot in common:
  1. You need to pass parameters lexicographically
  2. You need to handle the case where the return value is not id but one to the route and one to a specific module.
Compared to dealperformSelectorThere are, of course, differences:
  1. Break through theperformSelectorYou can only pass a limit of one parameter, and you can customize the format you want
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params;Copy the code
  1. Specific method call, protocol to call a layer

    handleActionThe method depends on the specificactionInstead ofperformSelectorDistribute the action.

But I still find the second one convenient, because your performSelector is also decoupled from the method that you’re actually calling. Let’s say one day you change the method: performSelector and you have to change the whole url to make sure that you’re calling the right Selector. The protocol does not. You can do secondary routing inside the handleAction method.


Call way

  • Middleware invocation module

I’ve made two schemes here, one pure Url and one with arguments

UIViewController *vc = [self openUrl:[NSString stringWithFormat:@"https://www.bilibili.com/KTModuleHandlerForA/getUserViewController?userName=%@&age=%d",userName,age]] ; NSNumber *value = [self openUrl:@"ModuleHandlerForLogin/loginIfNeed" params:@{@"delegate":delegate}];Copy the code

Both approaches are used and the differences will be discussed later.

  • Intermodule call

It is also possible to call directly in the above way, but it is easy to write errors. By adding a Category to the middleware, the interface is constrained. The url and parameter assembly work is handed over to the developer of the corresponding module.

@interface KTComponentManager (ModuleA)

- (UIViewController *)ModuleA_getUserViewControllerWithUserName:(NSString *)userName age:(int)age;

@endCopy the code

Then directly substitute the Category interface of the middleware

UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18];
    [self.navigationController pushViewController:vc animated:YES];Copy the code

Routing policy of middleware

  • Remote routing && Degraded routing
- (id)openUrl:(NSString *)url{ id returnObj; NSURL * openUrl = [NSURL URLWithString:url]; NSString * path = [openUrl.path substringWithRange:NSMakeRange(1, openUrl.path.length - 1)]; NSRange range = [path rangeOfString:@"/"]; NSString *targetName = [path substringWithRange:NSMakeRange(0, range.location)]; NSString *actionName = [path substringWithRange:NSMakeRange(range.location + 1, path.length - range.location - 1)]; // The URL can be routed. For example, deliver a JSON file from the server. If (self.redirectionjson[path]) {path = self.redirectionjson[path]; if (self.redirectionjson[path]; } / / if the target of the action has registered the if ([self. RegisteredDic [targetName] containsObject: actionName]) {returnObj = [the self openUrl:path params:[self getURLParameters:openUrl.absoluteString]]; } else if ([self webUrlSet containsObject: [nsstrings stringWithFormat: @ "% @ % @", openUrl. Host, openUrl. Path]]) {/ / low compatible version // If there are some H5 pages, open the H5 page //webUrlSet can be sent by the server NSLog(@" redirect webpage :%@",url); } return returnObj; }Copy the code

If the local version is too early, H5 needs to be forwarded to the remote route. If local routes are supported, local routes are directly used.

  • The local routing
- (id)openUrl:(NSString *)url params:(NSDictionary *)params { id returnObj; if (url.length == 0) { return nil; } // Urls can be routed. For example, deliver a JSON file from the server. If (self.redirectionjson[url]) {url = self.redirectionjson[url]; if (self.redirectionjson[url]; } NSRange range = [url rangeOfString:@"/"]; NSString *targetName = [url substringWithRange:NSMakeRange(0, range.location)]; NSString *actionName = [url substringWithRange:NSMakeRange(range.location + 1, url.length - range.location - 1)]; Class targetClass = NSClassFromString(targetName); [targetClass respondsToSelector:@selector(handleAction:params:)]) {output Target&&Action message returnObj = [targetClass handleAction:actionName params:params]; }else {// Unregistered, further processing. NSLog(@" unregistered method "); } return returnObj; }Copy the code

The action action and the parameter params are passed by calling the middleware protocol method handleAction: Params that the module entry module targetClass follows.


Module entry

Module entry implements middleware protocol method handleAction: Params: according to different actions, internal responsible for logic processing.

#import "ModuleHandlerForLogin.h" #import "KTLoginManager.h" #import "KTComponentManager+LoginModule.h" @implementation ModuleHandlerForLogin /** equivalent to each module maintaining its own registry */ + (id)handleAction:(NSString *)action params:(NSDictionary *)params {id returnValue = nil; if ([action isEqualToString:@"getUserViewController"]) { returnValue = [[KTAModuleUserViewController alloc]initWithUserName:params[@"userName"] age:[params[@"age"] intValue]]; } return returnValue; }Copy the code

Compatibility with lower versions

Sometimes a lower version of an App can be remotely routed without a native page.

In this case, if there is an H5 page, go to H5

/ / if the target of the action has registered the if ([self. RegisteredDic [targetName] containsObject: actionName]) {returnObj = [self openUrl: path params:[self getURLParameters:openUrl.absoluteString]]; } else if ([self webUrlSet containsObject: [nsstrings stringWithFormat: @ "% @ % @", openUrl. Host, openUrl. Path]]) {/ / low compatible version // If there are some H5 pages, open the H5 page //webUrlSet can be sent by the server NSLog(@" redirect webpage :%@",url); }Copy the code

RegisteredDic maintains the registry and records which Target && actions are implemented by the local module. This registration action is given to each module’s entry:

/* / + (void)load {#pragma Clang Diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" Class KTComponentManagerClass = NSClassFromString(@"KTComponentManager"); SEL sharedInstance = NSSelectorFromString(@"sharedInstance"); id KTComponentManager = [KTComponentManagerClass performSelector:sharedInstance]; SEL addHandleTargetWithInfo = NSSelectorFromString(@"addHandleTargetWithInfo:"); NSMutableSet * actionSet = [[NSMutableSet alloc]initWithArray:@[@"getUserViewController"]]; NSDictionary * targetInfo = @{ @"targetName":@"KTModuleHandlerForA", @"actionSet":actionSet }; [KTComponentManager performSelector:addHandleTargetWithInfo withObject:targetInfo]; #pragma clang diagnostic pop }Copy the code

Redirected routing

For some reason, sometimes we need to change the direction of certain Url routes (e.g. hitch rides?).

// The URL can be routed. For example, deliver a JSON file from the server. If (self.redirectionjson[path]) {path = self.redirectionjson[path]; if (self.redirectionjson[path]; }Copy the code

This redirectionJSON is delivered by the server. If a Path that needs to be redirected is found in a local route, the route is redirected and the destination of the route is changed.


Project structure

Modules are all introduced as private Pods, and individual modules internally follow MVC(MVP, MVVM, whatever you want). Just don’t introduce stuff from other modules).

I just wrote a demo, so I didn’t bother with my Pods. Intentions.


Degree of modularity

Each module, after the introduction of the public module. It can run independently in its own Target project.


Which modules are suitable for sinking

Modules that can be used across products

Logging, networking layer, third-party SDK, persistence, sharing, tool extensions, and more.


About Collaborative Development

Pods need to keep the version clear, like a new Category even if only one entry has been updated.

Therefore, it is best not to use Pods during the development phase because of the frequent code updates. You can write categories that work in your module’s Target first.

Finally, upload the Pods and pack them when debugging and launching.


Results demonstrate

I wrote three buttons

- (IBAction)pushToModuleAUserVC:(UIButton *)sender { if (! [[KTComponentManager sharedInstance] loginIfNeedWithDelegate:self]) { return; } UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18]; [self.navigationController pushViewController:vc animated:YES]; } - (IBAction)LoginBtnClick:(UIButton *)sender { if ([[KTComponentManager sharedInstance] loginIfNeedWithDelegate:self])  { [[KTComponentManager sharedInstance] loginOutWithDelegate:self]; } } - (IBAction)openWebUrl:(id)sender { [[KTComponentManager sharedInstance] openUrl:[NSString stringWithFormat:@"https://www.bilibili.com/video/av25305807"]]; } / / here should use the notice for the - (void) didLoginIn {[self. LoginBtn setTitle: @ "log out" forState: UIControlStateNormal]; } - (void) didLoginOut {[self loginBtn setTitle: @ "login" forState: UIControlStateNormal]; }Copy the code

Demo


The last

This article is mainly my own study and summary. If there is a flaw in the text, hope the message will be corrected. If you are willing to supplement and generous instruction little brother will be more grateful.