最近碰到一个问题是发现当移除一个subview
的时候,viewWillDisappear
被调用,但是viewDidDisappear
却没有被调用,导致注册的通知没有被注销,进而引发一系列的错误调用。趁此机会理清了一下有关"Child View Controller"的一些概念以及正确的调用方法。
Child View Controller
官方手册UIViewControler定义了 "View Controller"的主要职责,包括:更新视图内容;响应用户操作;调整视图尺寸以及控制页面布局。
对于我们这个Case中使用孩子视图控制器的行为在手册中叫做"Container View Controller",定义为:
A container view controller manages the presentation of content of other view controllers it owns, also known as its child view controllers
A child'��s view can be presented as-is or in conjunction with views owned by the container view controller.
引入addChildViewController
事实上是为了更好的管理addSubView
: 使得View
与ViewController
可以一一对应,可以使用多Controller从而避免混在一起,可以在暂时不需要显示视图的时候不载入,而等到需要的时候再在家,并且通过这个还可以更加方便的进行视图切换,减少代码耦合。(于此对应,iOS5之前,即使不显示,所有的view也都被加载在内存中)
ViewController容器篇这篇文章的总结蛮好,介绍了addChildViewController
相关的API的使用规则:
//添加
[self addChildViewController: _currentVC];
//[_currentVC willMoveToParentViewController: self];(自动调用 省略)
//[_currentVC didMoveToParentViewController: self]; (可省略)
//移除
[_currentVC willMoveToParentViewController: nil];
[_currentVC removeFromParentViewController];
//[_currentVC didMoveToParentViewController: nil]; (自动调用 省略)
//转换
[_currentVC willMoveToParentViewController: nil];
[self transitionFromViewController: _currentVC toViewController: _secondVC];
[_secondVC didMoveToParentViewController: self];
//转换子视图控制器
- (void)transitionFromOldViewController:(UIViewController *)oldViewController toNewViewController:(UIViewController *)newViewController{
[self transitionFromViewController:oldViewController toViewController:newViewController duration:0.3 options:UIViewAnimationOptionTransitionCrossDissolve animations:nil completion:^(BOOL finished) {
if (finished) {
[newViewController didMoveToParentViewController:self];
_currentVC = newViewController;
}else{
_currentVC = oldViewController;
}
}];
}
使用转换时,不需要移除"Child View Controller",保持多个"Child View Controller",并在之间切换是这个功能的目的之一。需要注意的是如何使用"MoveTo",而这里有一个重要的益处就是不需要再手动管理addSubView仅需在第一次初始化时使用,后面转换时不需要。
为了检测这些方法和孩子视图控制器之间的调用映射,写了下面的代码进行了测试:
var viewControllerOne:TestOneViewController? = TestOneViewController()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(clickAction), name:NSNotification.Name.UIApplicationWillResignActive, object: nil)
if let currentViewController = viewControllerOne {
self.addChildViewController(currentViewController)//加入self.childViewControllers, 不导致调用
self.view.addSubview(currentViewController.view)//导致调用 viewWillAppear/viewDidAppear
currentViewController.didMove(toParentViewController: self)
}
}
func clickAction() {
if let currentViewController = viewControllerOne {
currentViewController.willMove(toParentViewController: nil)
currentViewController.view.removeFromSuperview()//导致调用 viewWillDisappear/viewDidDisappear
currentViewController.removeFromParentViewController()//移出self.childViewControllers, 不导致调用
viewControllerOne = nil//导致调用 deinit
}
}
可以总结说:添加或者删除孩子视图控制器的行为不会导致孩子视图控制器的Action,而添加或者删除视图会直接导致相应的Action,而要保证孩子视图占用内存被及时释放,需要显示注销任何的引用。
Navigation Controller
UINavigationController
事实上就是一个"Container View Controller",而它提供了"Push/Pop"两个方法来方便的添加和移除"Child View Controller",可以直接翻译成上面的添加移除代码。
在"popViewController"模式下,是无法将最底层的"View Controller"也移除的,也就是至少保留一个:
如果希望去除所有的"Child View Controller",可以通过重置viewControllers
实现,另外一点需要注意的是,在这种情况下,仅仅最上层的那个会调用"viewWillDisappear/viewDidDisappear":
从这里也会发现,我们是不需要手动调用removeFromSuperview
的,这些可以自动完成。
引用
Apple UIViewControler
Custom Container View Controller
How to remove UIViewControllers from stack so ViewDidDisappear is called?