iOS-present和dismiss

先看总结:

  1. A推出B,A是主动方,B是被动方。A.presentedViewController = B,B.presentingViewController = A。
  2. 使用present去弹模态视图的时候,只能用最顶层的的控制器去弹,用底层的控制器去弹会失败,并抛出警告。
  3. 关于dismiss:
    ① 父节点负责调用dismiss来关闭他弹出来的子节点,你也可以直接在子节点中调用dismiss方法,UIKit会通知父节点去处理。
    ② 如果你连续弹出多个节点,应当由最底层的父节点调用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 = ?
问题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 <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)

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

个人理解:

如果A推出B:
A.presentedViewController 就是A的被动方是谁, 既然A推出B了, 那么A肯定是主动方,B是被动方, 所以A.presentedViewController = B
B.presentingViewController 就是B的主动方是谁, 同理B.presentingViewController = A

---------------------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 <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。

问题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. 关闭多个子节点时,只有最顶层的子节点会有动画效果,下层的子节点会直接被移除,不会有动画效果。

经过我的测试,确实如此。

这里讲一下使用场景:

如果我们需要弹出一个独立的流程,比如在A转账界面需要绑定安全设备,那绑定安全设备这些步骤就可以当成一个流程,可以通过A推出一个绑定Navi的B界面,在B再通过self.navigationController,push其他控制器,最后在A界面dismiss就可以直接把所有绑定安全设备流程dismiss掉

一个常见的错误:

下面这个错误很容易遇到吧。

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];
}

原文地址:https://www.jianshu.com/p/455d5f0b3656

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容