如何構建優雅的ViewController
前言
關於ViewController討論的最多的是它的肥胖和臃腫,即使使用傳統的MVC模式,ViewController也可以寫的很優雅,這無關乎設計模式,更多的是你對該模式理解有多深,你對於職責劃分的認知是否足夠清晰。ViewController也從很大程度上反應一個程式設計師的真實水平,初級程式設計師他的ViewController永遠是臃腫的、肥胖的,什麼功能都可以往裡面塞,不同功能間缺乏清晰的界限。而一個優秀的程式設計師它的ViewController顯得如此優雅,讓你產生一種竟不能修改一筆一畫的感覺。
ViewController職責
- UI 屬性配置 和 佈局
- 使用者互動事件
- 使用者互動事件處理和回撥
使用者互動事件處理: 通常會交給其他物件去處理 回撥: 可以根據具體的設計模式和應用場景交給 ViewController 或者其他物件處理
而通常我們在閱讀別人ViewController
程式碼的時候,我們關注的是什麼?
- 控制元件屬性配置在哪裡?
- 使用者互動的入口位置在哪裡?
- 使用者互動會產生什麼樣的結果?(回撥在哪裡?)
所以從這個角度來說,這三個功能一開始就應該是被分離的,需要有清晰明確的界限。因為誰都不希望自己在查詢互動入口的時候 ,去閱讀一堆控制元件冗長的控制元件配置程式碼, 更不願意在一堆程式碼中去慢慢理清整個使用者互動的流程。 我們通常只關心我當前最關注的東西,當看到一堆無關的程式碼時,第一反應就是我想註釋掉它。
基於協議分離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()
}
}
複製程式碼
這裡 我將控制元件的新增和控制元件的配置分成兩個函式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 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,儘量收集在同一個區域中, 同時對於每個回撥加上必要的註釋:
- 由哪種操作觸發
- 會導致什麼後果
- 最終會通往哪裡
所以從這個角度來說UITableViewDataSource
和 UITableViewDelegate
完全是兩種不一樣的行為, 一個是 configure UI,一個是 control behavior,所以不要在把這兩個東西寫一塊了, 真的很難看。
總結
基於職責對程式碼進行分割,這樣會讓你的程式碼變得更加優雅簡潔,會大大減少一些萬金油程式碼的出現。減少閱讀程式碼的成本也是我們優化的一個方向,畢竟誰都不想因為混亂的程式碼影響自己的心情