iOS 聊聊present和dismiss

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

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

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

  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,会有什么样的结果?

下文将逐个解答。

问题2:presentingViewController和presentedViewController属性

我们先看看问题2。UIViewController有两个属性,presentedViewController和presentingViewController。看文档的注释或许你能明白,反正楼主不太明白,明白了也容易忘记,记不住。

//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
B
A.presentingViewController (null)
A.presentedViewController
B.presentingViewController
B.presentedViewController (null)
---------------------B弹C后---------------------
C
A.presentingViewController (null)
A.presentedViewController
B.presentingViewController
B.presentedViewController
C.presentingViewController
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)

从上面的结果可以得出,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是顶层,没有子节点

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

如果A已经弹了B,这个时候想要在弹一个C,正确的做法是,B弹C。

如果你尝试用A弹C,系统会抛出警告,并且界面不会有变化,即C不会被弹出,警告如下:

Warning: Attempt to present on which is already presenting

把警告内容翻译一下,
"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 .'

经过排查我发现,如果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 on 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生命周期详解

如果觉得这篇文章对你有帮助,请点个赞吧。
转载请注明出处,谢谢!

你可能感兴趣的:(iOS 聊聊present和dismiss)