1. 程式人生 > IOS開發 >如何構建優雅的ViewController

如何構建優雅的ViewController

前言

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

ViewController職責

  • UI 屬性配置 和 佈局
  • 使用者互動事件
  • 使用者互動事件處理和回撥

使用者互動事件處理: 通常會交給其他物件去處理 回撥: 可以根據具體的設計模式和應用場景交給 ViewController 或者其他物件處理

而通常我們在閱讀別人ViewController程式碼的時候,我們關注的是什麼?

  1. 控制元件屬性配置在哪裡?
  2. 使用者互動的入口位置在哪裡?
  3. 使用者互動會產生什麼樣的結果?(回撥在哪裡?)

所以從這個角度來說,這三個功能一開始就應該是被分離的,需要有清晰明確的界限。因為誰都不希望自己在查詢互動入口的時候 ,去閱讀一堆控制元件冗長的控制元件配置程式碼, 更不願意在一堆程式碼中去慢慢理清整個使用者互動的流程。 我們通常只關心我當前最關注的東西,當看到一堆無關的程式碼時,第一反應就是我想註釋掉它。

基於協議分離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可能就是這樣


    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,所以不要在把這兩個東西寫一塊了, 真的很難看。

總結

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