如何構(gòu)建優(yōu)雅的ViewController
前言
關(guān)于ViewController討論的最多的是它的肥胖和臃腫,但是哪怕是采用MVC模式,ViewController同樣可以寫的很優(yōu)雅,這無關(guān)乎設(shè)計模式,對于那些以設(shè)計模式論高低的,我只能呵呵。其實這關(guān)乎的是你對設(shè)計模式的理解有多深,你對于職責(zé)劃分的認(rèn)知是否足夠清晰。ViewController也從很大程度上反應(yīng)一個程序員的真實水平,一個平庸的程序員他的ViewController永遠(yuǎn)是臃腫的、肥胖的,什么功能都可以往里面塞,不同功能間缺乏清晰的界限。而一個優(yōu)秀的程序員它的ViewController顯得如此優(yōu)雅,讓你產(chǎn)生一種竟不能修改一筆一畫的感覺。
ViewController職責(zé)
- UI 屬性 和 布局
- 用戶交互事件
- 用戶交互事件處理和回調(diào)
用戶交互事件處理: 通常會交給其他對象去處理
回調(diào): 可以根據(jù)具體的設(shè)計模式和應(yīng)用場景交給 ViewController 或者其他對象處理
而通常我們在閱讀別人ViewController代碼的時候,我們關(guān)注的是什么?
- 控件屬性配置在哪里?
- 用戶交互的入口位置在哪里?
- 用戶交互會產(chǎn)生什么樣的結(jié)果?(回調(diào)在哪里?)
所以從這個角度來說,這三個功能一開始就應(yīng)該是被分離的,需要有清新明確的界限。因為誰都不希望自己在查找交互入口的時候 ,去閱讀一堆控件冗長的控件配置代碼, 更不愿意在一堆代碼去慢慢理清整個用戶交互的流程。 我們通常只關(guān)心我當(dāng)前最關(guān)注的東西,當(dāng)看到一堆無關(guān)的代碼時,第一反應(yīng)就是我想注釋掉它。
基于協(xié)議分離UI屬性的配置
protocol MFViewConfigurer {
var rootView: UIView { get }
var contentViews: [UIView] { get }
var contentViewsSettings: [() -> Void] { get }
func addSubViews()
func configureSubViewsProperty()
func configureSubViewsLayouts()
func initUI()
}
依賴這個協(xié)議就可以完成所有控件屬性配置,然后通過extension protocol 大大減少重復(fù)代碼,同時提高可讀性
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()
}
}
這里 我將控件的添加和控件的配置分成兩個函數(shù)addSubViews和configureSubViewsProperty, 因為在我的眼里函數(shù)就應(yīng)該遵循單一職責(zé)這個概念:
addSubViews: 明確告訴閱讀者,我這個控制器只有這些控件
configureSubViewsProperty: 明確告訴閱讀者,控件的所有屬性配置都在這里,想要修改屬性請閱讀這個函數(shù)
來看一個實例:
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 這套協(xié)議同樣適用
```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模式還是有些區(qū)別,如果是MVC可能就是這樣
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 初始化 UI
initUI()
// 用戶交互事件入口
addEvents()
}
// MARK: callBack
......
由于MVC的回調(diào)模式很難統(tǒng)一,有Delegate, closure, notification 等等,所以回調(diào)通常會散落在控制器各個角落。最好加個MARK flag, 盡量收集在同一個區(qū)域中, 同時對于每個回調(diào)加上必要的注釋:
- 由哪種操作觸發(fā)
- 會導(dǎo)致什么后果
- 最終會留下哪里
所以從這個角度來說UITableViewDataSource 和 UITableViewDelegate 完全是兩種不一樣的行為, 一個是 configure UI , 一個是 control behavior , 所以不要在把這兩個東西寫一塊了, 真的很難看。
總結(jié)
基于職責(zé)對代碼進(jìn)行分割,這樣會讓你的代碼變得更加優(yōu)雅簡潔,會大大減少一些萬金油代碼的出現(xiàn),減少閱讀代碼的成本也是我們優(yōu)化的一個方向,比較誰都不想因為混亂的代碼影響自己的心情