#iOS componentization route decoupling thought JLRoutes practice part in App controller jump

Preface

The concepts of componentization and Router may have been relatively new a few years ago. So far, I believe that most students have been familiar with these terms. Before I really came into contact with Router and used it in the project, I also read some articles about the idea and framework of componentization and Router decoupling. However, due to my lack of practice, I failed to really apply them to the project Practice. As a result, every time I read the article, the knowledge I understood didn't really translate into problem-solving skills. I was lucky to contact and use the famous open source library in the project JLRouter To solve the jump logic between all pages inside and outside the App, after several years of learning and use, consolidate the knowledge while recording it, write it out to learn with you, and see the articles about using componentization Router shared on the Internet are partial to theory, there are few complete and detailed demos, and the specific use in the project needs further in-depth study, so with this article, what is there If it is not right or needs to be supplemented, I hope you can give me more advice.
This article focuses on actual combat and wants to learn more about the recommendation of Router thought written by frost God iOS componentization -- Analysis of routing design.

==Demo at the end of the article==

Why Router

The third question of routing Foundation: every time I touch the new idea framework, I can't help asking myself these questions. I hope that the following brief summaries can help you understand Router very well;

  • What is routing and what problems it solves


The above picture vividly shows the intricate relationship between each controller module in the project, which may be worse when we deal with it improperly
After using Router, it's like this;

For example, router is the same as the router we use everyday. Each controller in the App can be imagined to have connected to different devices of the router. Of course, when connecting to the router, you need to enter a password. Router is the same. Before using, you need to register each device. Router saves the URL of each device internally. When different devices need to interact with each other, Send the message to the router for unified processing;
When the controllers need to jump interactively, they only need to send the corresponding URL address to the Router. The Router will address the information of the other party according to its registered URL, and then it is responsible for instantiating the object, passing the parameters, jumping and other work. Each controller does not need to depend on each other, and perfectly solves the coupling between different modules!

  • Why to use the origin of the road to realize VC jump
    There are many things Router can do. First of all, we use it to solve the difficult controller coupling relationship, which is a very effective solution;

In App, there are three kinds of controller jumps: Modal (presented / dismission), navigation controller (Push/pop), Storyboard (Segue), and UITabBarVC main controller Index switch;
In addition to the normal jump between controllers, there will be a 3D Touch specified jump to a controller;
Jump between apps: URL Scheme, Universal Links;
It can be imagined that whether it is page switching or external calling in App, it will involve controller jump, switch, etc;
Here's a common scenario for chestnuts:
Pseudo code before Router:

If the scenarios of A Push B and B Modal C are implemented before the introduction of Router: the general practice is to introduce B in A and C in B, and then a hard code is required before each jump,

//A push B a page to B page, and set the corresponding @ perpeoty, callback, etc;
#import "B"
B* BVC = [B new]; 
BVC.delegate = A;
BVC.name = @"jersey";
BVC.callback = ^ (id data) {
};
... 
...
... Yes b Set some business related parameters, delegate,  callback Wait;
[A.nav pushVC: BVC animation: true]; 
// B -> C 
#import "C"
C* CVC = [C new];
[B presentVC: CVC];
[B presentVC: CVC animation: true completion: nil];

==Pseudo code after Router:==

After quoting Router, in the same scenario, our code is like this; in the controller that needs to do jump, we will introduce our encapsulated = = jsdvcrouter (a layer of encapsulation for JLRouter, which is specially used to manage App jump classes, which will be explained in detail later in the article) = = OK

    // A Push B;
    #import "JSDVCRouter"
    [JSDVCRouter openURL: BVCPath info: @{@"delegate":self,@"name":@"jersey",@"callback":callback}];    
    // BVCPath: indicates the Path defined by us for the B controller, which is generally saved in the global Map. Each Path Map of the current controller Map contains relevant parameters such as title, class, needLog, etc;
    // B Modal C
    [JSDVCRouter openURL: C info: {kJSDRouteSegue: @"Modal"}]; // The jump between controllers is implemented by Push by default. When Modal is needed, a parameter is passed; 

The students who believe in reading carefully here have seen the advantages of using Router:

1. The coupling degree is reduced: controller A does not need to know the existence of controller B, but only needs to import "JSDRouter" to perform the corresponding jump logic and assignment, etc; 
2. Code readability improvement: of course, when you just touch it, you will not be used to it. After a period of contact, not only the number of lines of code is reduced, but also the readability is very high. Goodbye to push/pop, present/dismiss;
3. Improve code reusability: every time the controller jumps and assigns values, repetitive code is required once * * (seriously violating the reusability principle) * *), and the logic of jump and assignment is encapsulated by jsdrutter, once the code is used for life;
4. Easy to maintain: it's a bit tangled at this point. When the project grows with the company's scale, the number of controllers and the jump become more and more complex, the jump method and logic are easy to become more and more confusing, and the later management is more difficult. Using JSDVCRouter's single responsibility principle to take charge of all jumps in the App can effectively improve testing and later maintenance. Of course, the cost is to maintain RouterMap and improve JSDVCRouter's internal logic; 
5. Dynamic and flexibility: when using Router, you can cooperate with the Key of the background response to transfer the response to determine the real page to jump, rather than the way of hard coding to jump;
6. To be added: 
  • How many steps does it take to implement Router to complete controller jump?
    Switch the controller to Router for the first time

It's very simple. There are only three steps. If the demand changes little, it's almost once and for all;

  1. Map table creation: it is a global map. The Path of the corresponding controller in the app is defined. The router can map the map made by the corresponding controller according to the Path. The map contains at least the parameters of the current controller, such as: {@ "Class": @ "controller Class name"}. When calling this route, a set of bound maps is obtained as parameters, and the instance is initialized by Class;
    The code structure is as follows:
     + (NSDictionary *)configInfo
        return @{ JSDRouteHomeCenter: @{
                     @"class": @"JSDAHomeCenterVC",
                     @"name": @"home page",
                     @"parameter": @"",
                     @"needLogin": @"0", },
                  JSDRouteUserLogin: @{
                     @"class": @"JSDUserLoginVC",
                     @"name": @"Land",
                     @"parameter": @"",
                     @"needLogin": @"0", },
        };
  1. Encapsulate JLRouter; for the convenience of use, management, and later migration, etc.! Similar to the use of AFNetwork, SDWebImage, MJRefresh and other famous open-source libraries, because the open-source libraries provide very rich functions, but we may actually use only one or two of its main functions to solve the problems in the project, we will use them according to the company's specific business scenarios or usage habits , to make it more suitable for the actual requirements;
    The author has carried on a layer of encapsulation + Category: jsdvcroouter, jsdvcroouter + add;

JSDVCRouter: mainly used to declare Router calling interface;
JSDVCRouter + Handle: it is mainly used to implement Router registration, handle skip and parameter assignment codes between controllers;

  1. Jump according to the agreed Path: after the above 1 and 2 are ready, you can easily jump to the controller [jsdvcrotter OpenURL: BVC];

Post maintenance of business change

  1. Map maintenance: with the development of business, when a new page is added, add a specified Path and corresponding binding parameters to the map;
  2. JSDVCRouter maintenance: the code that contains the real initialization jump and assignment of the controller is rarely modified here; for example, in the later stage, it needs to support the jump to H5, handle 3D touch and universal links to maintain here;

Code!

Writing here, I don't know whether the above brief introduction of Router's controller jump can help the students who are in initial contact with Router. I hope that the following Code can make you better understand and use it!
The following details the three classes that the author encapsulates JLRoutes to realize controller jump:

JSDVCRouterConfig

This file is mainly used to manage the configuration files (title,needLogin, etc.) of all routes mapped to the specified controller class name and related parameters. The specific configuration can be carried out according to the actual project requirements;

  1. In order to better detect errors during compilation, use the extern NSString const declaration to specify Router URL with NSString const implementation. When using, directly use the constant string declared externally to specify jump;
  1. In this way, it is more convenient to read and maintain the Router URL. If you directly use the @ "/ login" method to bind, you will easily get errors caused by carelessness;

    The code is as follows:

    //All controllers in App
    extern NSString* const JSDVCRouteWebview;
    extern NSString* const JSDVCRouteLogin;
    @interface JSDVCRouterConfig : NSObject

    + (NSDictionary *)configMapInfo;
    
    @end
    
    //Relevant controller in App
    NSString* const JSDVCRouteWebview = @"/webView";
    NSString* const JSDVCRouteLogin = @"/login";
    @implementation JSDVCRouterConfig
    
    + (NSDictionary *)configMapInfo {
    
    return @{
        JSDVCRouteWebview: @{@"class": @"JSDWebViewVC",
                             @"title": @"WebView",
                             @"flags": @"",
                             @"needLogin": @"",
        },
        JSDVCRouteLogin: @{@"class": @"JSDLoginVC",
                           @"title": @"Sign in",
                           @"flags": @"",
                           @"needLogin": @"",
        },
        };
    @end 
JSDVCRouter

The internal implementation of this class is very simple. It inherits from NSObject, provides external registration and calling of Router interface, and internally calls the interface provided by JLRoutes;

All jumps in the project use the interface provided by this class to call Router;

One is without any parameters by default 
Another can carry the parameters we need (NSDictionary);
[JSDVCRouter openURL:JSDVCRouteAppear]; //push to AppearVC; 
[JSDVCRouter openURL:JSDVCRouteAppear parameters:@{kJSDVCRouteSegue: kJSDVCRouteSegueModal, @"name": @"jersey"}]; // Modal to Appear VC with parameter name;

To encapsulate the benefits of a JSDVCRouter separately:

It is inherited from NSObject and does not directly depend on JLRouter to prevent the invasion of the three-party library. In the later stage, if the three-party library is to be replaced or a set of functions similar to those provided by JLRouter is to be encapsulated by itself, it only needs to be modified, and other places do not need to be modified;

@interface JSDVCRouter : NSObject

    + (BOOL)openURL:(NSString *)url;//Call Router;
    + (BOOL)openURL:(NSString *)url parameters:(NSDictionary *)parameters;
    
    + (void)addRoute:(NSString* )route handler:(BOOL (^)(NSDictionary *parameters))handlerBlock;//Register the Router, and the callback will be triggered when calling the Router; 

    @end
    #define JSDRouterURL(string) [NSURL URLWithString:string]

    @implementation JSDVCRouter
    
    + (BOOL)openURL:(NSString *)url {
        
        return [self routeURL:url parameters:nil];
    }
    
    + (BOOL)openURL:(NSString *)url parameters:(NSDictionary *)parameters {
        
       return [self routeURL:url parameters:parameters];
    }
    
    + (void)addRoute:(NSString *)route handler:(BOOL (^)(NSDictionary * _Nonnull parameters))handlerBlock {
        
        [JLRoutes addRoute:route handler:handlerBlock];
    }

    #pragma mark - mark JLRouter
    
    + (BOOL)routeURL:(NSString*)url parameters:(NSDictionary *)parameters{
        
        return [JLRoutes routeURL:JSDRouterURL(url) withParameters:parameters];
    }
    
    @end
    

JSDVCRouter+Handle

This is where the logic implementation of handling callback controller jump and parameter assignment is put when the Router is actually registered and called.

Register Router: register all routers in the controller one by one, switch TabBarIndex, process and return to router, and forward the callback to the defined method.

Processing Router: that is to say, after registering the router, when calling the corresponding router, we write a callback method when registering. Here is the logic of executing controller jump and parameter transfer.

About controller jump: when the Router is triggered, we can get the Map mapped by the Router, get its Class, and initialize the instance initially by Class. Here, we can find the current visibleVC for Push or Modal by UIViewController Category, and we can also determine whether to Push or Modal and whether to execute according to the parameters passed by the business side Line animation and so on;

About parameter passing: the passed parameter is the data structure of the dictionary, so we first check whether the instance VC contains this attribute, [VC responsedstoselector: nsselectorfromstring (Key)]. If the VC has this attribute, it will directly use KVC to assign the value. In order to prevent the mismatch between the passed dictionary Key and VC attribute, some bugs will be added, and a layer of NSAssert will be added, In this way, problems can be found faster in the development process!

There may be some inconsistencies in the controller jump logic encapsulated by the author, which should be judged according to the specific business requirements;

The following are the callback processing codes after registering Router and matching to Router respectively. Please read them patiently for a long time

Register Router to unify the three types of callback processing

    @implementation JSDVCRouter (Handle)
    //Register Router, jump of controller + UITabBarIndex switch + page return
    + (void)load {
        [self performSelectorOnMainThread:@selector(registerRouter) withObject:nil waitUntilDone:false];
    }
    + (void)registerRouter {
    
    //Get global RouterMapInfo
    NSDictionary* routerMapInfo = [JSDVCRouterConfig configMapInfo];
    
    // router corresponds to the path of the controller, which is used to register Route. When calling the current Route, a callback will be executed. Callback parameters: parameters passed in when executing Route;
    for (NSString* router in routerMapInfo.allKeys) {
        
        NSDictionary* routerMap = routerMapInfo[router];
        NSString* className = routerMap[kJSDVCRouteClassName];
        if (JSDIsString(className)) {
            /*Register all controllers Router, use [JSDVCRouter openURL:JSDVCRouteAppear]; push to AppearVC;
            [JSDVCRouter openURL:JSDVCRouteAppear parameters:@{kJSDVCRouteSegue: kJSDVCRouteSegueModal, @"name": @"jersey"}];  Modal Go to Appear VC and carry the parameter name;
             */
            [self addRoute:router handler:^BOOL(NSDictionary * _Nonnull parameters) {
                //After the route matching is successful, jump to logical callback;
                /*Execute Route callback; process controller jump + parameter transfer;
                ** routerMap: The routeMap of the current route Map; the Map we configured in RouterConfig;
                ** parameters: Parameters passed in when route is called;
                 */
                return [self executeRouterClassName:className routerMap:routerMap parameters:parameters];
            }];
        }
    }
    
    // Register Router to the specified TabBar Index; use [JSDVCRouter openURL:JSDVCRouteCafeTab] to switch to Cafe Index
    [self addRoute:@"/rootTab/:index" handler:^BOOL(NSDictionary * _Nonnull parameters) {
        NSInteger index = [parameters[@"index"] integerValue];
        // Handle UITabBarControllerIndex switch;
        UITabBarController* tabBarVC = (UITabBarController* )[UIViewController jsd_rootViewController];
        if ([tabBarVC isKindOfClass:[UITabBarController class]] && index >= 0 && tabBarVC.viewControllers.count >= index) {
            UIViewController* indexVC = tabBarVC.viewControllers[index];
            if ([indexVC isKindOfClass:[UINavigationController class]]) {
                indexVC = ((UINavigationController *)indexVC).topViewController;
            }
            //Biography
            [self setupParameters:parameters forViewController:indexVC];
            tabBarVC.selectedIndex = index;
            return YES;
        } else {
            return NO;
        }
    }];
    // Register to return to the upper page Router, use [JSDVCRouter openURL:kJSDVCRouteSegueBack] to return to the previous page or [JSDVCRouter openURL:kJSDVCRouteSegueBack parameters:@{kJSDVCRouteBackIndex: @(2)}] to return to the first two pages
    [self addRoute:kJSDVCRouteSegueBack handler:^BOOL(NSDictionary * _Nonnull parameters) {
        
        return [self executeBackRouterParameters:parameters];
    }];
    }

Call back after the Router is matched: instantiate the controller, assign parameters, page Jump

#pragma mark - execute Router VC    
// When the specified Router is found, the route callback logic will be triggered; if the registered Router is not found, NO will be returned directly; if necessary, a callback that is not matched to the Router globally can also be registered here for exception handling;
+ (BOOL)executeRouterClassName:(NSString *)className routerMap:(NSDictionary* )routerMap parameters:(NSDictionary* )parameters {
    // Intercepts the route mapping parameter, whether to log in to jump;
    BOOL needLogin = [routerMap[kJSDVCRouteClassNeedLogin] boolValue];
    if (needLogin && !userIsLogin) {
        [JSDVCRouter openURL:JSDVCRouteLogin];
        return NO;
    }
    //Unified initialization controller, transfer and jump;
    UIViewController* vc = [self viewControllerWithClassName:className routerMap:routerMap parameters: parameters];
    if (vc) {
        [self gotoViewController:vc parameters:parameters];
        return YES;
    } else {
        return NO;
    }
}
// Implement the controller according to the class name mapped by Router;
+ (UIViewController *)viewControllerWithClassName:(NSString *)className routerMap:(NSDictionary *)routerMap parameters:(NSDictionary* )parameters {
    
    id vc = [[NSClassFromString(className) alloc] init];
    if (![vc isKindOfClass:[UIViewController class]]) {
        vc = nil;
    }
#if DEBUG
    //vc is not UIViewController
    NSAssert(vc, @"%s: %@ is not kind of UIViewController class, routerMap: %@",__func__ ,className, routerMap);
#endif
    //parameter assignment
    [self setupParameters:parameters forViewController:vc];
    
    return vc;
}
// Assign value to VC parameter
+ (void)setupParameters:(NSDictionary *)params forViewController:(UIViewController* )vc {
    
    for (NSString *key in params.allKeys) {
        BOOL hasKey = [vc respondsToSelector:NSSelectorFromString(key)];
        BOOL notNil = params[key] != nil;
        if (hasKey && notNil) {
            [vc setValue:params[key] forKey:key];
        }
        
#if DEBUG
    //vc has no corresponding attribute, but passed value
        if ([key hasPrefix:@"JLRoute"]==NO &&
            [key hasPrefix:@"JSDVCRoute"]==NO && [params[@"JLRoutePattern"] rangeOfString:[NSString stringWithFormat:@":%@",key]].location==NSNotFound) {
            NSAssert(hasKey == YES, @"%s: %@ is not property for the key %@",__func__ ,vc,key);
        }
#endif
    };
}
// Jump and parameter setting;
+ (void)gotoViewController:(UIViewController *)vc parameters:(NSDictionary *)parameters {
    
    UIViewController* currentVC = [UIViewController jsd_findVisibleViewController];
    NSString *segue = parameters[kJSDVCRouteSegue] ? parameters[kJSDVCRouteSegue] : kJSDVCRouteSeguePush; //  Decide present or Push; default Push
    BOOL animated = parameters[kJSDVCRouteAnimated] ? [parameters[kJSDVCRouteAnimated] boolValue] : YES;  // Transition animation;
    NSLog(@"%s Jump: %@ %@ %@",__func__ ,currentVC, segue,vc);
    
    if ([segue isEqualToString:kJSDVCRouteSeguePush]) { //PUSH
        if (currentVC.navigationController) {
            NSString *backIndexString = [NSString stringWithFormat:@"%@",parameters[kJSDVCRouteBackIndex]];
            UINavigationController* nav = currentVC.navigationController;
            if ([backIndexString isEqualToString:kJSDVCRouteIndexRoot]) {
                NSMutableArray *vcs = [NSMutableArray arrayWithObject:nav.viewControllers.firstObject];
                [vcs addObject:vc];
                [nav setViewControllers:vcs animated:animated];
                
            } else if ([backIndexString integerValue] && [backIndexString integerValue] < nav.viewControllers.count) {
                //Remove the specified number of VC in Push;
                NSMutableArray *vcs = [nav.viewControllers mutableCopy];
                [vcs removeObjectsInRange:NSMakeRange(vcs.count - [backIndexString integerValue], [backIndexString integerValue])];
                nav.viewControllers = vcs;
                [nav pushViewController:vc animated:YES];
            } else {
                [nav pushViewController:vc animated:animated];
            }
        }
        else { //Because there is no navigation bar, directly execute Modal
            BOOL needNavigation = parameters[kJSDVCRouteSegueNeedNavigation] ? NO : YES;
            if (needNavigation) {
                UINavigationController* navigationVC = [[UINavigationController alloc] initWithRootViewController:vc];
                //vc.modalPresentationStyle = UIModalPresentationFullScreen;
                [currentVC presentViewController:navigationVC animated:YES completion:nil];
            }
            else {
                //vc.modalPresentationStyle = UIModalPresentationFullScreen;
                [currentVC presentViewController:vc animated:animated completion:nil];
            }
        }
    }
    else { //Modal
        BOOL needNavigation = parameters[kJSDVCRouteSegueNeedNavigation] ? parameters[kJSDVCRouteSegueNeedNavigation] : NO;
        if (needNavigation) {
            UINavigationController* navigationVC = [[UINavigationController alloc] initWithRootViewController:vc];
            //vc.modalPresentationStyle = UIModalPresentationFullScreen;
            [currentVC presentViewController:navigationVC animated:animated completion:nil];
        }
        else {
            //vc.modalPresentationStyle = UIModalPresentationFullScreen;
            [currentVC presentViewController:vc animated:animated completion:nil];
        }
    }
}

Can insist to see here, should carry on the controller jump to the Router already had a good understanding!

To be supplemented

In addition to frequent controller to controller switching, App internal jump also includes jumping to H5 or WebView;
The App external jump includes Scheme startup, 3D touch, universal link, click notification, etc;
These include jump and page switching. We can use Router for effective management, making App more dynamic and coupling between modules lower;

  • [] support H5 jump
  • [] external Scheme starts App
  • [ ] UniversalLink
  • [ ] 3D Touch Shortcut
  • [] support the background dynamic distribution of RouterMap configuration

Last

I hope this article can help you, if there is something wrong, I hope you can leave a message to point out the correction.

emmmmm, every time you see such a long string of code, you can either directly skip it, or you can start Xcode Coding and practice common + R once after you read it seriously. For your convenience
Demo If you think it will help you, please give a Star thank you!!!!!

On the way to study, I would like to share with you!!!

Author: Jersey . welcome to reprint, please indicate the source and Link in this article

Reference - link

iOS componentization -- Analysis of routing design
iOS architecture practice dry goods
On the componentization scheme of iOS application architecture
The Componentization of mushroom Street App
Actual combat Demo If you think it will help you, please give a Star thank you!!!!!

Tags: iOS Attribute xcode

Posted on Thu, 09 Jan 2020 00:43:29 -0800 by noober