1 职责
视图控制器, 顾名思义它是控制视图的专用对象.
视图控制器中有一个是 window 的 root VC, 这个 VC 就是顶层 VC, 其主要作用是:
决定是否根据用户操作来旋转屏幕: 运行时会查询顶层控制器, 由顶层控制器决定是否允许进行屏幕旋转.
操作状态栏: 运行时查询顶层控制器, 如果在顶层控制器中有配置的话, 这些配置决定是否显示状态栏, 以及状态栏的文字颜色.
在这里又两种声音, 一种将 ViewController 作为 MVC 中的控制器使用, 这样的情况下需要额外关注控制器是否职责过多(初学者比较容易写出巨大的控制器), 另外一类是将 ViewController 仅用于控制视图, 而独立出一系列 MVC 中的控制器对象, 由控制器对象中包含业务逻辑. 类似于 MVP 中的 P 类对象.
2 视图控制器层次
在 iOS 中, 有两种控制器间关系:
包含关系: 一个控制器作为其他控制器的容器使用, 比如导航控制器, Tabbar 控制器, 也可以自定义容器控制器. 作为容器的控制器称为 "父控制器", 包含在容器内的控制器称为 "子控制器". 子控制器的视图如果要显示的话, 需要将子控制器视图加入到父控制器的视图树中. 由父控制器负责将子控制器视图显示到界面.
显示关系(modal): 一个控制器将另外一个控制器显示出来. 被显示的控制器称为 presented VC, 而显示它的控制器称为 presenting VC.
通过上述两种关系, 可以构成控制器层级, 由一个根控制器往下延伸, 从而形成一颗视图控制器树.
视图控制器的层次在某种意义上就直接对应了视图树的层次关系.
多数情况下, 不需要手动将控制器视图添加到视图树中, 因为可以自动完成这个工作. 只需要手动控制器视图控制器的层次即可. 就好比将根控制器添加到 window 上, 根控制器的主视图就自动变成了 window 的子视图是一个道理.
自动添加视图的时候, 会自动将子控制器的视图尺寸修改为和自己视图尺寸一致(window 的话是和 window 尺寸一致).
但在自定义容器控制器时, 有时需要手动将子控制器的视图添加到视图树中.
3 视图控制器如何获取其视图
当创建出控制器的时候, 实际它是没有主视图的. 同时视图控制器对象相对视图对象而言体积比较小, 因为界面元素会占据大量内存(相对而言). 故视图控制器会推迟到需要获取它的视图时再获取视图, 即懒加载视图.
如果要在运行时查询其视图, 而不触发控制器加载视图的话, 就查询视图控制器的 viewIfLoaded
属性, 这个属性会返回一个视图或 nil, 且不会触发加载视图.
如果要显式触发加载视图的话, 就调用 loadViewIfNeeded()
方法.
只要是触发了 view 加载, 加载完成后就会触发 viewDidLoad
方法. 但需要注意的是, 此时视图控制器的主视图可能还未加入到视图树中, 且绝大多数情况下都是(此时 view 的 window 属性还是 nil)!
因此, 不应在 viewDidLoad
中进行一些依赖于屏幕尺寸或窗口尺寸的操作(初学者常犯的错误).
获取视图有三种方式:
自动由系统创建空的 UIView
重写
loadView
方法提供使用 nib 文件加载.
其中使用 nib 文件加载时:
- 新建一个 View 类型的 xib 文件
- 将 xib 文件中的 File's Owner 的类型设置为对应控制器的类型
- 将 File's Owner 的 view 属性连接到 xib 文件中的 view. 具体操作就是在 File's Owner 上按住 ctrl 并拖线到 View 上.
- 在代码中通过
DemoViewController(nibName: "DemoVCViewA", bundle: nil)
这样的方式新建 视图控制器即可使用. 其中参数代表的就是这个控制器及其视图是通过该 nib 文件加载进来的. 即在 nib 文件中的 file owner 代表控制器类型, 控制器就被 nib 初始化出来, 同时连接了视图的那根线, 故视图也是在 nib 中加载.
另外苹果有一个规定, 只要 xib 文件名和视图控制器文件名相同, 就可以自动加载 xib 中的视图作为控制器主视图. 故如果 xib 名为 DemoViewController.xib, 且控制器名为 DemoViewController, 则控制器初始化时可以直接写 DemoViewController()
而无需指定参数.
而使用如下的代码可以达到动画隐藏状态栏的效果:
class DemoViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let btn = UIButton()
btn.frame = CGRect(100, 100, 100, 100)
btn.backgroundColor = .blue
view.addSubview(btn)
btn.addTarget(self, action: #selector(btnClick), for: .touchUpInside)
}
var hide = false
override var prefersStatusBarHidden: Bool {
return self.hide
}
@objc func btnClick() {
hide = !hide
UIView.animate(withDuration: 0.4) {
self.setNeedsStatusBarAppearanceUpdate() // 触发隐藏或显示状态栏, 隐藏状态栏的同时, 安全区会上移 20 个点.
self.view.layoutIfNeeded()
}
}
}
在 APP 中要支持旋转, 且非跟随系统时, 可以在根控制器中设置一个固定的旋转, 而在 APP 代理中的 supportedInterfaceOrientationsFor
方法中设置允许的旋转.
最佳实践是不要在 viewDidLoad
中写手动布局代码...因为在 viewDidLoad 的时候, view 并未加入到视图树中, VC 的 view 可能在之后重新被调整尺寸(resize), 但只在 viewDidLoad 中写自动布局是可以的. 还有就是配置 View 也可以.
但比如有时你必须在视图控制器写手动布局代码(或者必须在里面写)时, 那应该在什么位置写布局代码呢?
虽然 viewWillLayoutSubViews
是一个, 但它会在视图控制器的生命期中多次被调用...这里给了一个解法就是使用 optional 的 view, 然后在该方法中判断是否为空, 从而让布局代码只执行一次...
下面是 present VC 的例子, 条件都是利用控制器A (根视图的子视图) 来 present 控制器B :
let vcb = DemoViewControllerB()
vcb.modalTransitionStyle = .flipHorizontal
present(vcb, animated: true, completion: nil)
如果单独像上述代码那样, 则被显示的 VC 的视图会替换掉根控制器的视图, 这时根控制器的视图都没有显示在视图树内了:
如果像下面代码这样, 让控制器 A 定义上下文的话:
let vcb = DemoViewControllerB()
vcb.modalTransitionStyle = .flipHorizontal
definesPresentationContext = true
vcb.modalPresentationStyle = .currentContext
present(vcb, animated: true, completion: nil)
则控制器 B 显示后不会替换掉根控制器, 而是之后替换掉作为上下文的控制器 A:
关于 presented VC 和 presenting VC 之间的数据交流:
正向传递是非常简单的, 只需要往对应参数传值即可. 但从 presented 往 presenting 传递就要换种思路了, 一般的方式是使用代理回传, 即定义一个被显示控制器的代理协议, 然后显示控制器实现这个协议并作为被显示的控制器的代理, 即可进行数据传递.
这里遇到一个当从 presenting 代理请求到 presenter 中时, 如果执行了 dismiss, 则 presenter 的视图会被显示超过导航栏. 这个问题需要解决.
待续...