轻松处理界面跳转

一、前言

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容