一、前言
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
- 1、先上流程圖,也許你會遇到這些需求:VC_A --》VC_B --》VC_C,此時在某種需要場景下,需要 VC_C --》VC_A。
(下面說的界面刷新是指控制器的生命周期方法再走一遍)
(1)、界面不需要刷新,可以直接使用
PopToViewController
回去。(2)、此時界面需要刷新,需要傳值回去,并且刷新控制器的生命周期方法。
(3)、此時界面不需要刷新,需要傳值回去,不刷新生命周期方法
- 2、針對上面的第一個需求,如果此時不知道 VC_A 在棧中的下標(復雜界面很有可能,當然有辦法算出來),那么就很難通過
PopToViewController
回到 VC_A;針對第二個需求,傳值刷新問題,由于是多界面通訊,首先肯定想到是使用通知
,但通知相對來說就比較離散化了,一多起來就很不方便管理。
3、上面的需求其實很好解決,或許你也知道,就是使用
navigationController
的setViewControllers: 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兩層后,指定dismiss回到首層控制器,那很簡單,dismiss兩次就好了。但這樣的效果會很難受,實際上,我們只需要獲取到指定回到控制器的presentedViewController
,然后調用一下 dismiss 就好,那么如何實現呢?
1、參考系統導航控制器
UINavigationController
的做法,通過一個數組去控制管理,命名為FLPresentStackController
,因此對外API基本一致2、用法也和
UINavigationController
類似,初始化傳入rootViewController
,當然,為了適配系統present
,框架中做了適應,當不存在FLPresentStackController
的時候,就相當于系統 modal 用法。3、具體實現思路是,在
FLPresentStackController
中維護一個數組棧,當調用present
ordismiss
的時候,會對這個數組進行操作,進入實現多層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
為了提高用戶體驗,自定義轉場動畫是很常見的手段,這里并不是自定義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 地址, 喜歡我的文章可以點個贊,關注我,會不定時更新文章,謝謝。