iOS 中的视图控制器

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 文件加载时:

  1. 新建一个 View 类型的 xib 文件
  2. 将 xib 文件中的 File's Owner 的类型设置为对应控制器的类型
  3. 将 File's Owner 的 view 属性连接到 xib 文件中的 view. 具体操作就是在 File's Owner 上按住 ctrl 并拖线到 View 上.
  4. 在代码中通过 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 的视图会替换掉根控制器的视图, 这时根控制器的视图都没有显示在视图树内了:


情况1

如果像下面代码这样, 让控制器 A 定义上下文的话:

let vcb = DemoViewControllerB()
vcb.modalTransitionStyle = .flipHorizontal
definesPresentationContext = true
vcb.modalPresentationStyle = .currentContext
present(vcb, animated: true, completion: nil)

则控制器 B 显示后不会替换掉根控制器, 而是之后替换掉作为上下文的控制器 A:


情况2

关于 presented VC 和 presenting VC 之间的数据交流:

正向传递是非常简单的, 只需要往对应参数传值即可. 但从 presented 往 presenting 传递就要换种思路了, 一般的方式是使用代理回传, 即定义一个被显示控制器的代理协议, 然后显示控制器实现这个协议并作为被显示的控制器的代理, 即可进行数据传递.

这里遇到一个当从 presenting 代理请求到 presenter 中时, 如果执行了 dismiss, 则 presenter 的视图会被显示超过导航栏. 这个问题需要解决.

待续...

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

推荐阅读更多精彩内容