需求:A视图控制器中
present
B视图控制器,B视图控制器再present
C视图控制器。最后从C视图控制器直接返回到A视图控制器。
1. 一些解释
1.1 两个常用的方法
/**
* 展示模态视图
*
* @param viewControllerToPresent 调转的目标控制器
* @param flag 如果flag == NO,那么不会执行任何动画(当然也不会执行自定义动画);如果flag == YES,则有可能执行自定义动画,如果没有自定义动画则会执行系统默认动画。
* @param completion 跳转完成后的回调
*/
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion;
/**
* 关闭模态视图
*
* @param flag 如果flag == NO,那么不会执行任何动画(当然也不会执行自定义动画);如果flag == YES,则有可能执行自定义动画,如果没有自定义动画则会执行系统默认动画。
* @param completion 关闭完成后的回调
*/
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion;
1.2 两个名词说明
// The view controller that was presented by this view controller or its nearest ancestor.
@property(nullable, nonatomic,readonly) UIViewController *presentedViewController NS_AVAILABLE_IOS(5_0);
// The view controller that presented this view controller (or its farthest ancestor.)
@property(nullable, nonatomic,readonly) UIViewController *presentingViewController NS_AVAILABLE_IOS(5_0);
Apresent
B,A是presentedViewController,B是presentingViewController。
2. dismissViewController在哪里执行?
我们最常使用的一种场景:A视图控制器present
跳转到B视图控制器,B视图控制器dismiss
回到A视图控制器,所以会想当然的以为dismissViewController
这个方法是在B视图控制器里面执行。
标准答案是:在A视图控制器里执行。
苹果文档有这么一段话:
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, it automatically forwards the message to the presenting view controller.
或者我们只要记住一个简单的原则:
谁污染,谁治理!
但是,在上述场景中,用B视图控制器执行也是可以的,因为系统会自动优化,当B视图控制器没有present
过其他视图控制器的时候,dismissViewController
方法会自动交给B视图控制器的presentingViewController
执行,也就是A视图控制器。
3. 多个模态视图之间跳转
回到我们前言的需求,如何从C视图控制器直接返回到A视图控制器?
在网上也看到一些解答,比如说利用通知让B视图控制器执行dismissViewController
方法。其实这样是不行的,原因刚刚已经解释过,对于一个视图控制器X,它执行dismissViewController
方法的时候将会关闭它present
的模态视图,只有在它没有present
过模态视图的时候(但是这里B已经present
到C了),才会交给他的presentingViewController
执行dismissViewController
方法。所以这里如果交给B执行,和直接在C里面执行dismissViewController
方法的效果是一样的。
显然一个最简单的解决办法就是利用通知或者代理,在A中执行dismissViewController
方法。此时B和C视图控制器会发生什么变化呢?
依然摘录一段苹果的文档做一下解释:
If you present several view controllers in succession, thus building a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack. When this happens, only the top-most view is dismissed in an animated fashion; any intermediate view controllers are simply removed from the stack. The top-most view is dismissed using its modal transition style, which may differ from the styles used by other view controllers lower in the stack.
也就是说,其实在present
多个视图控制器的时候,系统维护了一个栈,以我们现在这个情况为例,从栈底到栈顶依次是A->B->C。当栈中某个位置的视图控制器执行dismissViewController
方法的时候,栈中所有在它之上的视图控制器都会被dismiss
,不同的是,栈顶的视图控制器将会以动画方式被dismiss
,而中间的视图控制器只是简单的remove
掉。
这种方法是可行的,因为 dismissViewController
总是要在A方法中执行的。不过这样做会遇到代码耦合的问题。
一种低耦合的解决方案:
// C视图控制器触发dismiss方法前添加这么一段代码
UIViewController *rootVC = self.presentingViewController;
// rootVC.view.alpha = 0;
while (rootVC.presentingViewController) {
rootVC = rootVC.presentingViewController;
}
[rootVC dismissViewControllerAnimated:YES completion:nil];
在循环中连续获取presentingViewController,于是最终可以得到根视图控制器,这里就是A,不过这使得A视图控制器中不用添加任何代码,从而解决了耦合的问题。
这样写的另一个好处是,不管是多少个视图控制器之间的跳转,都可以很方便的完成。
缺点:你会明显的感觉到是从C->B->A,有没有一种完美的解决方案,让用户感觉直接从C->A?
这句代码rootVC.view.alpha = 0;
的加入可以让达到C->A的效果,但是Animated的设置就无效了。
还是求完美的解决方法。