iOS 聊聊present和dismiss

今天遇到一个崩溃,最后发现是由于present弹了一个模态视图致使的。今天就总结一下关于present和dismiss相关的问题。swift

先列几个问题,你能答上来吗

假设有3个UIViewController,分别是A、B、C。下文中的“A弹B”是指
[A presentViewController:B animated:NO completion:nil];bash

  1. 若是A已经弹了B,这个时候你想在弹一个C,是应该A弹C,仍是B弹C,A弹C可不可行?
  2. 关于UIViewController的两个属性,presentingViewController和presentedViewController。
    若是A弹B,A.presentingViewController = ?,A.presentedViewController = ?,B.presentingViewController = ?,B.presentedViewController = ? 若是A弹B,B弹C呢?
  3. 若是A弹B,B弹C。A调用dismiss,会有什么样的结果?

下文将逐个解答。app

问题2:presentingViewController和presentedViewController属性

咱们先看看问题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.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)
复制代码

从上面的结果能够得出,presentingViewController属性返回相邻父节点,presentedViewController属性返回相邻子节点,若是没有父节点或子节点,返回nil。注意,这两个属性返回的是当前节点直接相邻父子节点,并非返回最底层或者最顶层的节点(这点和文档注释有出入)。下面对照例子解释下这个结论。动画

---------------------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是顶层,没有子节点
复制代码

子控制器childViewControllers

当一个控制器成为另外一个控制器(常见的如,UINavigationViewController、UITabBarController、UIPageViewController等)的子控制器时,子控制器的presentedViewController和presentingViewController属性由父控制器的presentedViewController和presentingViewController的属性决定。ui

例如 A present Navi(B),那么this

  1. B.presentingViewController = Navi.presentingViewController = A
  2. B.presentedViewController = Navi.presentedViewController = nil
  3. A.presentingViewController = nil
  4. A.presentedViewController = Navi

解释一下上面的例子
假如,A和B是UIViewController,Navi是UINavigationController,B是Navi的rootViewController。atom

  1. 由于B是Navi的子控制器,根据刚才的结论,子控制的presentingViewController和presentedViewController由父控制器决定,B的父控制器是Navi,Navi的presentingViewController是谁呢,根据以前的结论,presentingViewController属性返回相邻的父节点,即A。因此,B.presentingViewController = Navi.presentingViewController = A。
  2. 同理1.
  3. A没有父控制器,A.presentingViewController等于A相邻父节点,因为A没有父节点,因此A.presentingViewController = nil。
  4. A没有父控制器,A.presentedViewController等于A相邻子节点,A的子节点是Navi,因此A.presentedViewController = Navi。

问题1:present的层级问题,屡次弹窗由谁去弹

若是A已经弹了B,这个时候想要在弹一个C,正确的作法是,B弹C。spa

若是你尝试用A弹C,系统会抛出警告,而且界面不会有变化,即C不会被弹出,警告以下:

Warning: Attempt to present <UIViewController: 0x7fbcecc04e80> on <ViewController: 0x7fbcecd09850> which is already presenting <UIViewController: 0x7fbcef2024c0>

把警告内容翻译一下,
"尝试在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

问题3:dismiss方法

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];
}
复制代码

上述代码都会失败,B并不会弹出,并会抛出上面的警告。警告说得很明确,self.view尚未被添加到视图树(父视图),不容许弹出视图。
也就是说,若是一个viewController的view还没被添加到视图树(父视图)上,那么用这个viewController去present会失败,并抛出警告。

若是你非要这么写的话,能够把present的部分放到-viewDidAppear方法中,由于-viewDidAppear被调用时self.view已经被添加到视图树中了(强烈不推荐)。

正确的作法应该是使用childViewController,你能够用添加子视图、子控制器的方式来实现相似效果(推荐)。**

- (void)viewDidLoad {
    [super viewDidLoad];
 
    _BViewController = [[UIViewController alloc] init];
    _BViewController.view.frame = self.view.bounds;
    [self.view addSubview:_BViewController.view];
    [self addChildViewController:_BViewController];  //这句话必定要加,不然视图上的按钮事件可能不响应
}
复制代码

关于UIView的生命周期,viewDidLoad系列方法的调用顺序,能够参考这篇博文,写得很是好。UIView生命周期详解

若是以为这篇文章对你有帮助,请点个赞吧。若是有疑问能够关注个人公众号我留言。
转载请注明出处,谢谢!

参考连接
你真的了解iOS中控制器的present和dismiss吗?

相关文章
相关标签/搜索