前段时间遇到一个崩溃,最后发现是由于presentViewController弹了一个模态视图致使的。今天就总结一下关于present和dismiss相关的问题。swift
假设有3个UIViewController,分别是A、B、C。下文中的“A弹B”是指[A presentViewController:B animated:NO completion:nil];
bash
下文将逐个解答。app
咱们先看看问题2。UIViewController有两个属性,presentedViewController和presentingViewController。看文档的注释或许你能明白,反正楼主不太明白,明白了也容易忘记,记不住。iview
//UIKit.UIViewController.h
// 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);
复制代码
那本身写个Demo验证一下呗:咱们建立A、B、C三个试图控制器,上面分别放上按钮,点A上的按钮,A弹B,点B上的按钮,B弹C。结束时分别打印各自的presentedViewController和presentingViewController属性。结果以下:学习
---------------------A弹B后---------------------
A <ViewController: 0x7fe43ff0c9f0>
B <UIViewController: 0x7fe43ff05160>
A.presentingViewController (null)
A.presentedViewController <UIViewController: 0x7fe43ff05160>
B.presentingViewController <ViewController: 0x7fe43ff0c9f0>
B.presentedViewController (null)
---------------------B弹C后---------------------
C <UIViewController: 0x7fe43fd06190>
A.presentingViewController (null)
A.presentedViewController <UIViewController: 0x7fe43ff05160>
B.presentingViewController <ViewController: 0x7fe43ff0c9f0>
B.presentedViewController <UIViewController: 0x7fe43fd06190>
C.presentingViewController <UIViewController: 0x7fe43ff05160>
C.presentedViewController (null)测试
翻译一下动画
---------------------A弹B后---------------------
A.presentingViewController (null)
A.presentedViewController B
B.presentingViewController A
B.presentedViewController (null)
---------------------B弹C后---------------------
A.presentingViewController (null)
A.presentedViewController B
B.presentingViewController A
B.presentedViewController C
C.presentingViewController B
C.presentedViewController (null)ui
从上面的结果能够得出,presentingViewController属性返回父节点,presentedViewController属性返回子节点,若是没有父节点或子节点,返回nil。注意,这两个属性返回的是当前节点直接相邻父子节点,并非返回最底层或者最顶层的节点(这点和文档注释有出入)。下面对照例子解释下这个结论。this
---------------------A弹B后---------------------
A.presentingViewController (null) //由于A是最底层,没有父节点,因此A的父节点返回nil
A.presentedViewController B //B在A的上层,B是A的子节点,因此A的子节点返回B
B.presentingViewController A //B的父节点是A,因此B的父节点返回A
B.presentedViewController (null) //B没有子节点,因此B的子节点返回nil
---------------------B弹C后---------------------
A.presentingViewController (null) //A是最底层,没有父节点
A.presentedViewController B //A的直接子节点是B
B.presentingViewController A //B的父节点是A
B.presentedViewController C //B的子节点是C
C.presentingViewController B //C的直接父节点是B
C.presentedViewController (null) //C是顶层,没有子节点atom
若是A已经弹了B,这个时候想要在弹一个C,正确的作法是,B弹C。
若是你尝试用A弹C,系统会抛出警告,而且界面不会有变化,即C不会被弹出,警告以下:
Warning: Attempt to present <UIViewController: 0x7fbcecc04e80> on <ViewController: 0x7fbcecd09850> which is already presenting <UIViewController: 0x7fbcef2024c0>
把警告内容翻译一下,
"Warning: Attempt to present C on A which is already presenting B"
再翻译一下,
"尝试在A上弹C,可是A已经弹了B"
这下就很清楚了,使用present去弹模态视图的时候,只能用最顶层的的控制器去弹,用底层的控制器去弹会失败,并抛出警告。
我简单地写了个方法来获取传入viewController的最顶层子节点,你们能够参考下。
//获取最顶层的弹出视图,没有子节点则返回自己
+ (UIViewController *)topestPresentedViewControllerForVC:(UIViewController *)viewController
{
UIViewController *topestVC = viewController;
while (topestVC.presentedViewController) {
topestVC = topestVC.presentedViewController;
}
return topestVC;
}
复制代码
文章开头我提到过一个崩溃问题,下面是崩溃时Xcode的日志:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally an active controller <ViewController: 0x7feddce0c9e0>.'
通过排查我发现,若是present一个已经被presented的视图控制器就会崩溃。通常是不会出现这种情形的,若是出现了多是由于同一行present的代码被屡次执行致使的,注意检查,修复bug。
dismiss方法你们都很熟悉吧- (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion
通常,你们都是这么用的,A弹B,B中调用dismiss消失弹框。没问题。
那,A弹B,我在A中调用dismiss能够吗?——也没问题,B会消失。
那,A弹B,B弹C。A调用dismiss,会有什么样的结果?是C消失,仍是B、C都消失,仍是会报错?
——正确答案是B、C都消失。
咱们来看下官方文档对这个方法的说明。
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
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.
文档指出
1.父节点负责调用dismiss来关闭他弹出来的子节点,你也能够直接在子节点中调用dismiss方法,UIKit会通知父节点去处理。
2.若是你连续弹出多个节点,应当由最底层的父节点调用dismiss来一次性关闭全部子节点。
3.关闭多个子节点时,只有最顶层的子节点会有动画效果,下层的子节点会直接被移除,不会有动画效果。
通过个人测试,确实如此。
下面这个错误很容易遇到吧。
Warning: Attempt to present <UIViewController: 0x7fa43ac0bdb0> on <ViewController: 0x7fa43ae15de0> whose view is not in the window hierarchy!
你的代码多是这样的
- (void)viewDidLoad {
[super viewDidLoad];
_BViewController = [[UIViewController alloc] init];
[self presentViewController:_BViewController animated:NO completion:nil];
}
复制代码
或者这样的
- (void)viewWillAppear {
[super viewWillAppear];
_BViewController = [[UIViewController alloc] init];
[self presentViewController:_BViewController animated:NO completion:nil];
}
复制代码
上述代码都会失败,B并不会弹出,并会抛出上面的警告。警告说得很明确,self.view尚未被添加到视图树(父视图),不容许弹出视图。
也就是说,若是一个viewController的view还没被添加到视图树(父视图)上,那么用这个viewController去present会失败,并抛出警告。
理论上,不该该建立一个UIViewController时就present另外一个UIViewController。你能够用添加子视图、子控制器的方式来实现相似效果(推荐)。
- (void)viewDidLoad {
[super viewDidLoad];
_BViewController = [[UIViewController alloc] init];
_BViewController.view.frame = self.view.bounds;
[self.view addSubview:_BViewController.view];
[self addChildViewController:_BViewController]; //这句话必定要加,不然视图上的按钮事件可能不响应
}
复制代码
若是你非要这么写的话,能够把present的部分放到-viewDidAppear方法中,由于-viewDidAppear被调用时self.view已经被添加到视图树中了。
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
_BViewController = [[UIViewController alloc] init];
[self presentViewController:_BViewController animated:NO completion:nil];
}
复制代码
关于UIView的生命周期,viewDidLoad系列方法的调用顺序,能够参考这篇博文,写得很是好。UIView生命周期详解
做为一个开发者,有一个学习的氛围和一个交流圈子特别重要,这是个人交流群761407670(123),你们有兴趣能够进群里一块儿交流学习
原文做者:CocoaKier
连接:https://www.jianshu.com/p/455d5f0b3656