轻松处理界面跳转

作者: gitKong | 来源:发表于2017-09-01 16:21 被阅读629次

    一、前言

    • 1、一个项目中总会有出现界面跳转,常见的就是应用内跳转、Push、Modal、Segue,或者复杂的嵌套,考虑到方便项目的维护以及功能拓展,我觉得很有必要统一管理,本框架中的Facade类 就是管理所有跳转事件,其中Facade 是继承自NSObject的单例。

    • 2、统一管理一来方便功能拓展;二来整个项目可以保持统一代码风格,相对来说,可维护性更强;而且由于Facade 是继承自NSObject的单例,因此不依赖于控制器,耦合性更低,可以在任意类中实现跳转

    • 3、本框架着重封装了应用内跳转、Push和Modal方式,新增Embed方式,实现控制器嵌套跳转。至于Segue方式考虑到灵活性很差,项目中使用频率也低,因此不做封装。


    二、应用内跳转

    应用类跳转如果细分的话,可以分为跳转到苹果商店和其他App

    • 1、普通app(App Store以外)跳转

      - (void)openAppWithUrlScheme:(NSString *)urlScheme params:(NSDictionary<NSString *, id> *)params complete:(void(^)(BOOL success))complete;
      

      (1)跳转前需要配置 URL Schemes,这个就是跳转的url地址了,当然iOS 9.0 之后还需要配置白名单,在 info.plist 中配置 LSApplicationQueriesSchemes ,在iOS 10.0之后,新出跳转api :- (void)openURL:options:completionHandler:,相比之前的 - (BOOL)openURL:,实际上只是多了个 options 参数,options中的key:
      UIApplicationOpenURLOptionUniversalLinksOnly,可以设置布尔值,如果设置为YES,则只能打开应用里配置好的有效通用链接,此时如果没配置scheme,那么handler中就返回NO,本框架中默认使用系统的,相当于- (BOOL)openURL:用法。具体区别请自行查询,不详细分析。

      (2)值得提一下的是,app跳转一般需要进行参数传递,默认只能通过URL拼接 方式或者通过UIPasteboard(不建议),什么情况下使用UIPasteboard 呢,一般是用于图片传递的时候,不过其实没必要,本文的做法是通过将 UIImage 对象转成 NSString,然后进行参数拼接,其中本框架中还处理了:

      • 默认urlScheme 只需要传入配置在 info.plist 中的 URL Schemes 即可实现跳转,参数可以通过params 传入,框架会自动进行拼接处理。
      • 当然你也可以在urlScheme中拼接参数,此时如果params 不为空且合法,框架会默认在urlScheme中继续拼接,并实现跳转。
      • 如果此时自行拼接的参数和传入的params重复key,会以params为准,但跳转后的url不会进行裁剪,可以通过框架的 - (NSDictionary *)paramsByOpenAppWithUrl: 获取传入的参数。

    (3)关键代码如下:(逻辑都比较简单,不详细说明)

    - (void)openAppWithUrlScheme:(NSString *)urlScheme params:(NSDictionary<NSString *, id> *)params complete:(void(^)(BOOL success))complete {
      if (!urlScheme.isNotBlank) return;
      NSURL *url = [self urlWithScheme:urlScheme params:params];
      if (!url) return;
      if ([APPLICATION canOpenURL:url]) {
          if ([[[UIDevice currentDevice] systemVersion] compare:@"10.0" options:NSNumericSearch] == NSOrderedAscending) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated-declarations"
              BOOL success = [APPLICATION openURL:url];
    #pragma clang diagnostic pop
              if (complete) {
                  complete(success);
              }
          }
          else {
              [APPLICATION openURL:url options:@{} completionHandler:^(BOOL success) {
                  if (complete) {
                      complete(success);
                  }
              }];
          }
      }
      else {
          if (complete) {
              complete(NO);
          }
      }
    }
    
    • 2、App Store 跳转

      - (void)openAppleStoreWithIdentifier:(NSString *)identifier complete:(void(^)(BOOL success))complete;
      

      (1)众所周知,每个app在 App Store 中都有一个唯一的id,可以通过iTunes查看,那么此时只需要知道这个identifier 即可实现跳转。

      (2)跳转 App Store 其实也有两种方式,一种通过URL 跳转,一种通过 StoreKit 实现,两者区别就是,前者直接跳转到App Store,后者则在应用内打开,笔者觉得后者体验效果较优而且比较稳定,因此本框架中使用后者。而且为了优化体验效果,会先跳转过去,然后再加载数据。


    三、Push

    Push
    • 1、先上流程图,也许你会遇到这些需求:VC_A --》VC_B --》VC_C,此时在某种需要场景下,需要 VC_C --》VC_A。(下面说的界面刷新是指控制器的生命周期方法再走一遍)
      • (1)、界面不需要刷新,可以直接使用PopToViewController 回去。

      • (2)、此时界面需要刷新,需要传值回去,并且刷新控制器的生命周期方法。

      • (3)、此时界面不需要刷新,需要传值回去,不刷新生命周期方法

    • 2、针对上面的第一个需求,如果此时不知道 VC_A 在栈中的下标(复杂界面很有可能,当然有办法算出来),那么就很难通过PopToViewController 回到 VC_A;针对第二个需求,传值刷新问题,由于是多界面通讯,首先肯定想到是使用通知,但通知相对来说就比较离散化了,一多起来就很不方便管理。
    • 3、上面的需求其实很好解决,或许你也知道,就是使用navigationControllersetViewControllers: animated:方法,通过内部封装,对UINavigationController拓展,外界调用就十分方便,要实现上面的需求,只需要告诉我,是否需要popBack,此时reload重新刷新控制器,必须popBack为YES才有效。当然如果nav栈中不存在该控制器(框架中目前默认通过类名判断是否存在,并不是相同控制器),则执行系统Push方法。对于第三个需求,其实只需要通过 - (__kindof UIViewController *)viewControllerBy:(Class)vcClass 方法即可获取到栈中控制器,然后即可进行参数传递。

    • 4、关键代码(具体代码自行查看)

      - (void)popToIndex:(NSInteger)index thenPushViewController:(UIViewController *)viewController needBack:(BOOL)needBack needReload:(BOOL)needReload animated:(BOOL)animated complete:(void(^)())complete {
        NSArray *sourceViewControllers = self.viewControllers;
        if (index >= sourceViewControllers.count || viewController == nil || self.topViewController == viewController) {
            return;
        }
        __weak typeof(self) weakSelf = self;
        [self dispatch_afterViewControllerTransitionComplete:^{
            __strong typeof(weakSelf) strongSelf = weakSelf;
            NSMutableArray<UIViewController *> *arrM = [NSMutableArray arrayWithArray:sourceViewControllers];
            [sourceViewControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                if (idx > index) {
                    [arrM removeObject:obj];
                }
            }];
            if (needBack) {
                if (needReload) {
                    [strongSelf setViewControllers:arrM animated:animated];
                    if ([arrM.lastObject isKindOfClass:[viewController class]]) {
                        [arrM removeLastObject];
                    }
                    [arrM addObject:viewController];
                    [strongSelf setViewControllers:arrM animated:NO];
                }
                else {
                    [strongSelf setViewControllers:arrM animated:animated];
                }
            }
            else {
                [arrM addObject:viewController];
                [strongSelf setViewControllers:arrM animated:animated];
            }
        }];
        if (complete) {
            complete();
        }
      }
      

    三、Modal

    Present

    抛开需求谈功能都是不切实际,如上图,需求很简单,就是要 present两层后,指定dismiss回到首层控制器,那很简单,dismiss两次就好了。但这样的效果会很难受,实际上,我们只需要获取到指定回到控制器的presentedViewController,然后调用一下 dismiss 就好,那么如何实现呢?

    • 1、参考系统导航控制器 UINavigationController的做法,通过一个数组去控制管理,命名为 FLPresentStackController,因此对外API基本一致

    • 2、用法也和UINavigationController 类似,初始化传入 rootViewController,当然,为了适配系统 present,框架中做了适应,当不存在 FLPresentStackController 的时候,就相当于系统 modal 用法。

    • 3、具体实现思路是,在 FLPresentStackController 中维护一个数组栈,当调用 present or dismiss 的时候,会对这个数组进行操作,进入实现多层dismiss,跟导航控制器的做法是一样的。

    • 4、为了优化体验效果,使用的时候有个注意点,最后present的控制器中的视图控件,需要添加到 presentContentView 中,此时dismiss的时候就不会有视觉差,当然,如果你有更优的方案,欢迎留言。

      @property (nonatomic, strong, readonly) UIView *presentContentView;
      
    • 5、关键代码如下:

      - (void)dismissToIndex:(NSInteger)index animated: (BOOL)flag completion: (void (^)(void))completion {
        if (self.statckControllers && self.statckControllers.count && index >= 0 && index < self.statckControllers.count)  {
            NSInteger nextIndex = index + 1;
            if (nextIndex >= self.statckControllers.count) {
                return;
            }
            UIView *contentView = self.topViewController.presentContentView;
            UIViewController *currentViewController = self.statckControllers[index];
            UIViewController *nextViewController = self.statckControllers[nextIndex];
            if (contentView) {
                [nextViewController.view addSubview:contentView];
                [nextViewController.view bringSubviewToFront:contentView];
            }
            [currentViewController dismissViewControllerAnimated:flag completion:^{
                [contentView removeFromSuperview];
            }];
            NSArray<UIViewController *> *tempArr = [NSArray arrayWithArray:self.statckControllers];
            [tempArr enumerateObjectsUsingBlock:^(UIViewController * _Nonnull vc, NSUInteger idx, BOOL * _Nonnull stop) {
                if (idx > index) {
                    self.topViewController.presentStackController = nil;
                    [self.statckControllers removeObject:vc];
                }
            }];
            if (completion) {
                completion();
            }
        }
      }
      

    四、Embed

    Embed

    为了提高用户体验,自定义转场动画是很常见的手段,这里并不是自定义modal,这个是我自己理解的一种转场方式,其实就是嵌套控制器,并且提供多种转场动画。实现起来很简单,代码也比较简单,大家自行查看源码。

    • 值得提一下,框架中默认不能重复embed相同的控制器(相同类名),关键代码如下:
    - (void)embedViewController:(UIViewController *)vc inParentViewController:(UIViewController *)parentVC animateType:(FLFacadeAnimateType)animateType duration:(NSTimeInterval)duration completion:(void (^)())completion {
        if (vc.parentViewController == parentVC || [self isEmbedViewController:vc isExitAt:parentVC needJudgePrecision:NO]) {
            return;
        }
        
        [parentVC addChildViewController:vc];
        
        [vc willMoveToParentViewController:parentVC];
        
        [self embedView:vc.view atParentView:parentVC.view animateType:animateType];
        
        if (animateType == FLFacadeAnimateTypeNone) {
            [vc didMoveToParentViewController:parentVC];
        }
        else if([self isFadeAnimate:animateType]) {
            [self fadeAnimateWithView:vc.view atParentView:parentVC.view animateType:animateType duration:duration isEmbedAnimated:YES completion:^{
                [vc didMoveToParentViewController:parentVC];
            }];
        }
        else {
            [self transitionWithView:parentVC.view animateType:animateType duration:duration isEmbedAnimated:YES completion:^{
                [vc didMoveToParentViewController:parentVC];
            }];
        }
        if (completion) {
            completion();
        }
    }
    

    五、总结

    • 1、Facade 类继承自 NSObject,因此理论上来说可以在任何文件中实现跳转,前提是app当前有控制器并且已经加载完毕(本框架是通过 UIApplication 分类获取当前控制器去实现的)。

    • 2、框架是对系统跳转功能进行拓展并统一管理,因此内部兼容系统方法(其实都是系统方法),方便处理常见的跳转方式。

    • 3、框架中代码量不多,而且逻辑比较简单,因此没有做详细分析,大家如果有什么不明白或者错漏的地方可以留言或者简信我。

    • 4、Facade 地址, 喜欢我的文章可以点个赞,关注我,会不定时更新文章,谢谢。

    相关文章

      网友评论

      • 蒂埃里:少了两个文件,怎么玩?UIViewController+PresentStack.m和UIViewController+PresentStack.h
        gitKong:有呀:flushed:
      • 春暖花已开:不错,赞一个
        gitKong:@bit_tea 哈哈哈,这么严重
        bit_tea:建议把第一条放在最后,不然,看完第一句这篇文章就已经索然无味了...
        gitKong:@人民重重 谢谢支持:relaxed:

      本文标题:轻松处理界面跳转

      本文链接:https://www.haomeiwen.com/subject/vulhxxtx.html