美文网首页
<iOS 实践经验>利用子VC和主VC来管理一个sc

<iOS 实践经验>利用子VC和主VC来管理一个sc

作者: 貘鸣 | 来源:发表于2017-11-19 12:40 被阅读148次

    这篇文章的主要目的是从概念上来介绍如何将单个 VC 中的功能分解到若干个子VC中, 并实现各个VC的交互, 从而让VC的体积能真正不再无限制增长.

    对于某个 VC, 必须明确:

    1. 若只能执行一次的操作, 比如创建子视图, 添加子视图到视图树等, 必须放在只执行一次的方法中, 比如 viewDidLoad 中. 千万不要把这些东西放在会潜在执行多次的方法内.
    2. 除了 root View 外的 VC 视图属性创建: 要么在 SB 中, 要么在属性初始化时, 要么在 viewDidLoad 中, 千万不要在其他地方, 防止多次创建.

    一些基本要求:

    1. 什么时候要调用 super: 如果帮助中没有说一定要调用, 就不用调用. 这个要查询苹果帮助文档.... 没什么可说的. 比如 viewDidLoad 等就需要调用 super, 而 viewWillLayoutSubviews 就不用. 但是貌似基本上就 VC 生命期对应的那几个方法需要调用 super.
    2. viewWillLayoutSubviews 的调用次数: 如果在里面用指定frame的方式, 则首次 Layout 时会调用两次, 而如果用自动约束, 则首次 Layout 时只会调用一次. 混合方式下, 首次 Layout 时也只会调用一次.
    3. 在 VC 中如果想布局除视图树 root View 外的子视图, 则必须放在 viewWillLayoutSubviews 中, 注意仅布局, 不要把其他的操作放进去(比如配置外观或创建视图, 或是 addsubview 等), 否则可能多次执行, 但布局的改变必须进行多次执行, 这个是可以的.

    下面就来看如何在一个主VC(Flow VC, 即工作流VC)中, 添加若干子控制器, 用于控制主VC对应的 screen 里面的每个小视图. 这样的话, 以前一个 VC 里面的功能就能被分解到多个子 VC 中, 从而让 Massive ViewController 的时代成为过去.

    1 基本情况描述

    比如有这样的一个设计:


    概念性描述

    现在就来利用一个主 VC 和两个子 VC 来实现上面的设计.
    (下面为了简单起见, 每个子VC中都只含有一个控件, 并且控件的尺寸都是和子VC的 view 尺寸相同, 即充满view.)

    2 实现

    首先实现主 VC, 它主要负责管理子VC, 并将子VC的视图添加到它的 view 上面, 然后添加一个 Label 控制器, 和一个 Button 控制器. 最后来看控制器间的交流问题.

    2.1 添加 Label 控制器

    首先来添加 Label VC:

    
        private var _labelVC: LabelViewController!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            _labelVC = LabelViewController()
            addChildViewController(_labelVC)
            view.addSubview(_labelVC.view)
            _labelVC.didMove(toParentViewController: self)
        }
    
        override func viewWillLayoutSubviews() {
            _labelVC.view.frame = CGRect(x: 10, y: 10, width: 100, height: 100)
        }
    

    上面的代码首先创建一个 LabelViewController, 并通过 addChildViewController 添加到子VC数组中管理, addChildViewController 中会触发调用子 VC 的willMove(toParentViewController) 方法.

    然后设置子 VC 的 view 的frame, 注意这里 _labelVC.view.frame 中访问了子VC的view, 故在设置 frame 之前, 会先触发子VC创建其view, 接着就是子VC的viewDidLoad方法被触发. 所以在子VC的viewDidLoad中是不会获得我们设置的frame的. 故这里也是为什么上面要强调必须在 viewWillLayoutSubviews 中布局子视图的原因.

    接着将子VC的view加入到主VC视图树. 最后调用子VC的didMove(toParentViewController)方法, 可以在里面进行一些需要的行为. 因为在 didMove 方法调用之前, 子VC中还没有执行过viewWillLayoutSubviews.

    在子VC中, 即可进行视图布局以及相应操作了:

    class LabelViewController: UIViewController {
    
        var label = UILabel()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.addSubview(label)
        }
    
        override func willMove(toParentViewController parent: UIViewController?) {
            print("will move")
        }
    
        override func didMove(toParentViewController parent: UIViewController?) {
            print("didMove")
        }
    
        override func viewWillLayoutSubviews() {
            print("willLayout")
            pLayoutLabel()
        }
    
        private func pLayoutLabel() -> Void {
            label.snp.makeConstraints {
                $0.edges.equalTo(self.view)
            }
        }
    
        override func viewDidLayoutSubviews() {
            print("did layout")
        }
    }
    

    子VC显示到屏幕上的情况是这样的:


    image.png

    至此, Label组件及其控制VC就添加到了主VC上面.
    利用相同的方式, 再添加一个 Button VC.

    2.2 添加 Button 控制器

    添加方式相同, 故直接展示代码:

    class ViewController: UIViewController {
    
        private var _labelVC: LabelViewController!
        private var _btnVC: BtnViewController!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // 添加 labelVC
            _labelVC = LabelViewController()
            addChildViewController(_labelVC)
            view.addSubview(_labelVC.view)
            _labelVC.didMove(toParentViewController: self)
            // 添加 btn VC
            _btnVC = BtnViewController()
            addChildViewController(_btnVC)
            view.addSubview(_btnVC.view)
            _btnVC.didMove(toParentViewController: self)
        }
    
        override func viewWillLayoutSubviews() {
            _labelVC.view.frame = CGRect(x: 10, y: 10, width: 100, height: 100)
            _btnVC.view.frame = CGRect(x: 200, y: 200, width: 44.0, height: 44.0)
        }
    }
    

    BtnViewController 的实现如下所示:

    class BtnViewController: UIViewController {
    
        let btn = UIButton()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            btn.setTitle("改变", for: .normal)
            btn.setTitleColor(.black, for: .normal)
            view.addSubview(btn)
        }
    
        override func willMove(toParentViewController parent: UIViewController?) {
            print("will move")
        }
    
        override func didMove(toParentViewController parent: UIViewController?) {
            print("didMove")
        }
    
        override func viewWillLayoutSubviews() {
            print("willLayout")
            btn.frame = view.bounds
        }
    
        override func viewDidLayoutSubviews() {
            print("did layout")
        }
    }
    

    现在屏幕上的状态如下:


    添加了 btn 之后

    下面就来实现当点击了按钮后改变 Label 上的文字, 用于演示当一个主VC有多个子VC的情况下, 如何协调各个子VC的沟通.
    (如果子VC是与另外的组件沟通, 则可以利用其他的方式进行, 这里只强调在同一个父VC上的各子VC间的交流机制).

    2.3 VC间的交流问题

    这里想实现的需求是当点击了 "改变" 按钮后, 可以改变 Label 上面的文字. 如果两个控件在同一个 VC 上, 当然这个工作非常简单, 但现在需要的是两个子VC间的交流.

    实现交流的前提是: 保证的是两个子VC间低耦合, 这样的话子VC更易重用, 更易维护, 更易测试, 更易理解.

    故肯定不能直接在 Btn 控制器中直接拿到 Label 控制器的 Label 然后操作. 那能不能在父VC里面直接协调各个子VC的工作呢? 或者是从父VC中获取其他子VC的内容?

    这里就要先看苹果的几条指导性原则: 详见这条链接Suggestions for Building a Container View Controller 一节.

    下面来看里面主要的两条原则:

    1. 作为容器的父VC不能访问子VC中除了root View 外的其他任何内容(即不能知道子VC中的其他细节).
    2. 保证子VC和父VC最小的交流, 如果需要子VC来改变父VC的内容, 则最好使用代理的方式, 即让子VC作为父VC的代理, 从而改变父VC的相应内容.

    上述苹果的原则也是为了VC之间的低耦合. 故需要从其他地方来考虑.

    通知机制? 肯定是一个选择.

    有没有更好的方式呢? 这个还需要不断去寻找.

    相关文章

      网友评论

          本文标题:<iOS 实践经验>利用子VC和主VC来管理一个sc

          本文链接:https://www.haomeiwen.com/subject/mtczmxtx.html