輕松處理界面跳轉

一、前言

  • 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閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容