你真的了解iOS中控制器的present和dismiss吗?

一、了解present和dismiss

一个iOS开发,这个控制器的打开和关闭,应该是接触UIKit所接触的第一个关于UIViewController的API,然而,你真的了解它吗?
同样的,本文默认你已经了解UIViewController的present和dismis方法,并多次运用。另外本文不再加OC语言的代码,毕竟都能读得懂swift。

二、谁来调用dismiss方法?

咱们从最基本的开始:一个控制器A,和一个控制器B。
A present B
让A打开B,在A中是这样写:

1
2
3
//A
let B = ViewController()
self.present(B, animated: true, completion: nil)

在B中需要关闭B的时候在B中这么写:

1
2
//B
self.dismiss(animated: true, completion: nil)

难道?这样写有问题?这….可能大多数开发者都是这么写,也没有出现过什么问题。但是…
YES,这样写在“理论”讲,是错误的。
别急,在实际上,这么写大多数情况是没有问题的,因为iOS系统帮我们做了很多。

三、presentingViewController和presentedViewController

想了解为什么说那么写在“理论上”是错误的,先从presentingViewController和presentedViewController说起。

为了不使本文枯燥,不引用大段描述和说明,只针对案例解释。当从A中弹出B后:

  1. self.presentingViewController: 在A中,就是nil;在B中,就是A
  2. self.presentedViewController:在A中,就是B;在B中,就是nil

那为什么说在B中调用dismiss在“理论上”是错的呢?因为有这么一个规则一定要记住!

谁污染,谁治理!

很熟悉对不对?MRC的法则,谁创建谁释放!所以A打开了B,当然是A来负责关闭!

正确写法是在A里调用self.dismiss(animated: true, completion: nil)

等等,之前都是B里调用?没错,dismiss方法系统会自动优化,当B视图控制器调用dismiss时,它并没有打开任何界面,就将dismissViewController方法会自动交给B的presentingViewController执行,也就是A来执行。
如果现在A没有打开B的话,调用dismissViewController时它没有presentedViewController,转交给它的presentingViewController,但是它也没有presentingViewController,所以dismiss就不执行了。swift中可选型能很好的解释了:

1
self.presentingViewController?.dismiss(animated: true, completion: nil) //presentingViewController为nil,后面的不执行

扩展:关于实例为空的时候方法不执行,可百度“iOS消息转发”了解。

既然这样,说这个有毛用?
如果你从A打开了B,从B打开了C,现在怎么直接回到A?
你很可能会通过一些手段让B执行dismiss方法,但你会得到错误的结果,因为你这里如果交给B执行dismiss方法,和直接在C里面执行dismiss方法的效果是一样的,也就是说你到了B的界面并没有到A。
SO,dismiss方法必须让需要回到的这个控制器来执行。那么一个控制器的presentingViewController一定是打开它的那个控制器吗?继续往后看。

四、presentingViewController是打开它的那个控制器吗?

从上文,A打开了B,A是B的presentingViewController,那是不是所有的控制器的presentingViewController都是调用presnet方法打开自己的那个控制器呢?
NO!但是刚才A打开了B,不是说A是B的presentingViewController吗?当然,不是说一个控制器A弹出一个控制器B,A就一定是B的presentingViewController。
吃屁吧你

别着急,看几个图片娱乐一下:
第一种
这时箭头指向的控制器的presenting是谁?
第二种
这时箭头指向的控制器的presenting是谁?

第一个图片是navigationController,第二个是tabbarController。又跟你想的不一样了?看来你真的了解的太少,这时咱们得了解一下子控制器了。

四、子控制器childViewControllers

刚才在了解presentingViewController和presentedViewController的时候,如果你用代码试了一下,会发现parentViewController(OC中,swift叫parent),父控制器。有父就有子,每个控制器都有一个属性,叫做childViewControllers,存放它的子控制器。这里对childViewControllers的使用方法使用场景使用优势不做探讨。我们假设你了解并多次使用了子控制器来显示复杂界面的。

但是实际上只要你学习iOS的UIKit,你就肯定已经多次,甚至经常使用了这个东西。
我们知道,在一个有导航控制器的控制器P中加入了子控制器Q,然后添加了Q的view,这个Q就可以使用self.navigationController进行跳转,Q是利用的P的导航控制器进行跳转。也就是说Q的关于控制器的操作都被P给处理了。

那新加入一个问题:如果在Q中present出来一个界面R,那这个R的presentingViewController是谁呢?
是的,是P。此时已经出现了刚才说的问题,Q打开了R,但是R的presentingViewController却是P。这时因为Q是P的子控制器。

那么就可以解释上面两个图片的结果为什么是navi控制器和tabbar控制器了。因为使用标签控制器和导航控制器,都是显示的子控制器的内容,一个带导航栏,一个带标签栏。
所有的控制器都有childViewControllers属性,保存了所有的子控制器,但是有些特殊的控制器,比如UIPageViewController、UINavigationController、UITabBarController等,还有一个viewControllers属性,实际内容和childViewControllers一样。这些特殊的控制器实际上不展示主要内容,主要内容由子控制器展示。
所以刚才说,只要你学了UIKit用了UINavigationController和UITabBarController,你就在使用childViewControllers在做界面的管理了。

因此,当某个控制器有父控制器的时候,它的presentingViewController是父控制器的presentingViewController。

说这么多,又有毛用?

image.png

比如你做过转场动画,会用到这么个方法animateTransition(using transitionContext: UIViewControllerContextTransitioning),当然你首先要做的,就是从context里获取toViewcontroller和fromViewController。如果你的这个转场是个modalPresent,此时你获取到的fromViewController可不一定是调用present的那个控制器,所以你要根据这个VC来做一些动画,可能就要有问题了。

如果是带UINavigationController,需要通过nav.visibleViewController获取nav当前的控制器,如果是带UITabBarController,需要通过tab.selectedViewController获取tab当前的控制器。

四、如何强制指定presentingViewController就是打开自己的那个?

我们需要了解一下这两个属性:

1
2
@property(nonatomic,assign) BOOL definesPresentationContext;
@property(nonatomic,assign) UIModalPresentationStyle modalPresentationStyle;

modalPresentationStyle属性决定了将要present的控制器以何种方式展现,默认值为UIModalTransitionStyleCoverVertical。如果把一个控制器的definesPresentationContext属性设置为YES,那么在需要进行UIModalPresentationCurrentContext类型的跳转的时候,UIKit会使用视图层级内的这个控制器来进行跳转。

1
2
avc.definesPresentationContext = false
avc.modalPresentationStyle = .currentContext

大功告成!现在presentingViewController能够获取到我们期望的对象了。