如何構建優雅的ViewController

前言

關於ViewController討論的最多的是它的肥胖和臃腫,即便使用傳統的MVC模式,ViewController也能夠寫的很優雅,這無關乎設計模式,更多的是你對該模式理解有多深,你對於職責劃分的認知是否足夠清晰。ViewController也從很大程度上反應一個程序員的真實水平,初級程序員他的ViewController永遠是臃腫的、肥胖的,什麼功能均可以往裏面塞,不一樣功能間缺少清晰的界限。而一個優秀的程序員它的ViewController顯得如此優雅,讓你產生一種竟不能修改一筆一畫的感受。程序員

ViewController職責

  • UI 屬性配置 和 佈局
  • 用戶交互事件
  • 用戶交互事件處理和回調

用戶交互事件處理: 一般會交給其餘對象去處理 回調: 能夠根據具體的設計模式和應用場景交給 ViewController 或者其餘對象處理設計模式

而一般咱們在閱讀別人ViewController代碼的時候,咱們關注的是什麼?markdown

  1. 控件屬性配置在哪裏?
  2. 用戶交互的入口位置在哪裏?
  3. 用戶交互會產生什麼樣的結果?(回調在哪裏?)

因此從這個角度來講,這三個功能一開始就應該是被分離的,須要有清晰明確的界限。由於誰都不但願本身在查找交互入口的時候 ,去閱讀一堆控件冗長的控件配置代碼, 更不肯意在一堆代碼中去慢慢理清整個用戶交互的流程。 咱們一般只關心我當前最關注的東西,當看到一堆無關的代碼時,第一反應就是我想註釋掉它。ide

基於協議分離UI屬性的配置

protocol MFViewConfigurer {
    var rootView: UIView { get }
    var contentViews: [UIView] { get }
    var contentViewsSettings: [() -> Void] { get }

    func addSubViews()
    func configureSubViewsProperty()
    func configureSubViewsLayouts()

    func initUI()
}


複製代碼

依賴這個協議就能夠完成全部控件屬性配置,而後經過extension protocol 大大減小重複代碼,同時提升可讀性函數

extension MFViewConfigurer {
    func addSubViews() {
        for element in contentViews {
            if let rootView = rootView as? UIStackView {
                rootView.addArrangedSubview(element)
            } else {
                rootView.addSubview(element)
            }
        }
    }

    func configureSubViewsProperty() {
        for element in contentViewsSettings {
            element()
        }
    }

    func configureSubViewsLayouts() {
    }

    func initUI() {
        addSubViews()
        configureSubViewsProperty()
        configureSubViewsLayouts()
    }
}


複製代碼

這裏 我將控件的添加和控件的配置分紅兩個函數addSubViewsconfigureSubViewsProperty, 由於在個人眼裏函數就應該遵循單一職責這個概念: 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 MFWeatherViewController: MFViewConfigurer {
    var contentViews: [UIView] { return [scrollView, cancelButton] }

    var contentViewsSettings: [() -> Void] {
        return [{
            self.view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7)
            self.scrollView.hiddenSubViews(isHidden: false)
        }]
    }

    func configureSubViewsLayouts() {
        cancelButton.snp.makeConstraints { make in
            if #available(iOS 11, *) {
                make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
            } else {
                make.top.equalTo(self.view.snp.top).offset(20)
            }

            make.left.equalTo(self.view).offset(20)
            make.height.width.equalTo(30)
        }

        scrollView.snp.makeConstraints { make in
            make.top.bottom.left.right.equalTo(self.view)
        }
    }

}


而對於UIView 這套協議一樣適用

```Swift
// MFWeatherSummaryView
    private override init(frame: CGRect) {
        super.init(frame: frame)

        initUI()
    }
    
    
// MARK: - UI

extension MFWeatherSummaryView: MFViewConfigurer {
    var rootView: UIView { return self }

    var contentViews: [UIView] {
        return [
            cityLabel,
            weatherSummaryLabel,
            temperatureLabel,
            weatherSummaryImageView,
        ]
    }

    var contentViewsSettings: [() -> Void] {
        return [UIConfigure]
    }

    private func UIConfigure() {
        backgroundColor = UIColor.clear
    }

    public func configureSubViewsLayouts() {
        cityLabel.snp.makeConstraints { make in
            make.top.centerX.equalTo(self)
            make.bottom.equalTo(temperatureLabel.snp.top).offset(-10)
        }

        temperatureLabel.snp.makeConstraints { make in
            make.top.equalTo(cityLabel.snp.bottom).offset(10)
            make.right.equalTo(self.snp.centerX).offset(0)
            make.bottom.equalTo(self)
        }

        weatherSummaryImageView.snp.makeConstraints { make in
            make.left.equalTo(self.snp.centerX).offset(20)
            make.bottom.equalTo(temperatureLabel.snp.lastBaseline)
            make.top.equalTo(weatherSummaryLabel.snp.bottom).offset(5)
            make.height.equalTo(weatherSummaryImageView.snp.width).multipliedBy(61.0 / 69.0)
        }

        weatherSummaryLabel.snp.makeConstraints { make in
            make.top.equalTo(temperatureLabel).offset(20)
            make.centerX.equalTo(weatherSummaryImageView)
            make.bottom.equalTo(weatherSummaryImageView.snp.top).offset(-5)
        }
    }
}



複製代碼

因爲我使用的是MVVM模式,因此viewDidLoad 和MVC模式仍是有些區別,若是是MVC可能就是這樣ui

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, 儘可能收集在同一個區域中, 同時對於每一個回調加上必要的註釋:spa

  • 由哪一種操做觸發
  • 會致使什麼後果
  • 最終會通往哪裏

因此從這個角度來講UITableViewDataSourceUITableViewDelegate 徹底是兩種不同的行爲, 一個是 configure UI , 一個是 control behavior , 因此不要在把這兩個東西寫一塊了, 真的很難看。設計

總結

基於職責對代碼進行分割,這樣會讓你的代碼變得更加優雅簡潔,會大大減小一些萬金油代碼的出現。減小閱讀代碼的成本也是咱們優化的一個方向,畢竟誰都不想由於混亂的代碼影響本身的心情

相關文章
相關標籤/搜索