如何构建优雅的ViewController

1,819 阅读3分钟

前言

关于ViewController讨论的最多的是它的肥胖和臃肿,即使使用传统的MVC模式,ViewController也可以写的很优雅,这无关乎设计模式,更多的是你对该模式理解有多深,你对于职责划分的认知是否足够清晰。ViewController也从很大程度上反应一个程序员的真实水平,初级程序员他的ViewController永远是臃肿的、肥胖的,什么功能都可以往里面塞,不同功能间缺乏清晰的界限。而一个优秀的程序员它的ViewController显得如此优雅,让你产生一种竟不能修改一笔一画的感觉。

ViewController职责

  • UI 属性配置 和 布局
  • 用户交互事件
  • 用户交互事件处理和回调

用户交互事件处理: 通常会交给其他对象去处理 回调: 可以根据具体的设计模式和应用场景交给 ViewController 或者其他对象处理

而通常我们在阅读别人ViewController代码的时候,我们关注的是什么?

  1. 控件属性配置在哪里?
  2. 用户交互的入口位置在哪里?
  3. 用户交互会产生什么样的结果?(回调在哪里?)

所以从这个角度来说,这三个功能一开始就应该是被分离的,需要有清晰明确的界限。因为谁都不希望自己在查找交互入口的时候 ,去阅读一堆控件冗长的控件配置代码, 更不愿意在一堆代码中去慢慢理清整个用户交互的流程。 我们通常只关心我当前最关注的东西,当看到一堆无关的代码时,第一反应就是我想注释掉它。

基于协议分离UI属性的配置


enum TreeView {

    case leaf(UIView)

    case node(UIView, [TreeView])

  


    @discardableResult func build() -> UIView {

        switch self {

        case .leaf(let view):

            return view

  


        case .node(let parent, let nodes):

            nodes.forEach {

                let nodeTree = $0.build()

                if let stackView = parent as? UIStackView {

                    stackView.addArrangedSubview(nodeTree)

                } else {

                    parent.addSubview(nodeTree)

                }

            }

            return parent

        }

    }

  


    var rootView: UIView {

        switch self {

        case .leaf(let view):

            return view

        case .node(let parent, _):

            return parent

        }

    }

}

protocol PFMViewConfigurer {

    var contentViews: TreeView { get }

  


    func addSubViews()

    func configureSubViewsProperty()

    func configureSubViewsLayouts()

  


    func initUI()

}

依赖这个协议就可以完成所有控件属性配置,然后通过extension protocol 大大减少重复代码,同时提高可读性


extension PFMViewConfigurer {


    func addSubViews() {

        _ = contentViews.build()

    }

  


    func configureSubViewsProperty() {

        // default: not any thing

    }

  


    func configureSubViewsLayouts() {

        // default: not any thing

    }

  


    func initUI() {

        addSubViews()

        configureSubViewsProperty()

        configureSubViewsLayouts()

    }

}


这里 我将控件的添加和控件的配置分成两个函数addSubViews configureSubViewsProperty, 因为在我的眼里函数就应该遵循单一职责这个概念: addSubViews: 明确告诉阅读者,我这个控制器包含哪些控件 configureSubViewsProperty : 明确告诉阅读者,控件的所有属性配置都在这里,想要修改属性请阅读这个函数

来看一个实例:


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

		 // 初始化 UI
        initUI()
	
		 // 绑定用户交互事件
        bindEvent()

		 // 将ViewModel.value  绑定至控件
        bindValueToUI()
       
    }
    
    // MARK: - UI configure

// MARK: - UI

extension PFMAccountsViewController: PFMViewConfigurer {

    var contentViews: TreeView {

        .node(

            view,

            [

                .node(

                    stackView,

                    [

                        .leaf(titleLabel),

                        .leaf(trailingIconImageView)

                    ]

                )

            ]

        )

    }

  


    func configureSubViewsLayouts() {

        stackView.anchor()

            .topToSuperview(constant: Constants.space)

            .leftToSuperview(constant: Constants.space)

            .rightToSuperview(constant: Constants.space)

            .bottomToSuperview(constant: Constants.space)

            .activate()

    }

}



由于我使用的是MVVM模式,所以viewDidLoad 和MVC模式还是有些区别,如果是MVC可能就是这样


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

		// 初始化 UI
        initUI()
	
		 // 用户交互事件入口
        addEvents()

       
    }
    
 // MARK: callBack
 ......

由于MVC的回调模式很难统一,有Delegate, Closure, Notification、KVC等,所以回调通常会散落在控制器各个角落。最好加个MARK flag, 尽量收集在同一个区域中, 同时对于每个回调加上必要的注释:

  • 由哪种操作触发
  • 会导致什么后果
  • 最终会通往哪里

所以从这个角度来说UITableViewDataSourceUITableViewDelegate 完全是两种不一样的行为, 一个是 configure UI , 一个是 control behavior , 所以不要把这两个东西写一块了。

总结

基于职责对代码进行分割,这样会让你的代码变得更加优雅简洁,会大大减少一些万金油代码的出现。减少阅读代码的成本也是我们优化的一个方向,毕竟谁都不想因为混乱的代码影响自己的心情