轻松处理界面跳转

一、前言

  • 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 地址, 喜欢我的文章可以点个赞,关注我,会不定时更新文章,谢谢。

推荐阅读更多精彩内容