一、前言
-
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、上面的需求其实很好解决,或许你也知道,就是使用
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抛开需求谈功能都是不切实际,如上图,需求很简单,就是要 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
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 地址, 喜欢我的文章可以点个赞,关注我,会不定时更新文章,谢谢。
网友评论