前言
自從入坑了 Flutter,瞭解了現代 web 框架,回頭來看 iOS 原生的命令式 UI 產能實在太低了,就好像騎自行車和汽車賽跑一樣。
問題出在哪?
- 沒有響應式,沒有
setState()
,這一點可以通過 RxSwift 的繫結來將就。 - 沒有宣告式,傳統的命令式 UI 的程式碼和效果不匹配。
- 沒有 JIT,編譯耗費大量時間。
命令式的問題在陶文的 物件導向不是銀彈,DDD 也不是,TypeScript 才是 中有更深入的討論:
Many states:數量上多
Concurrent / Parallel:併發是邏輯上的,並行是物理上的。無論是哪種,都比 sequential 更復雜。
Long range causality:長距離的因果關係
Entangled:剪不斷理還亂
SwiftUI 呢?
雖然 SwiftUI 很美,甚至支援了 Hot reload,但是遠水解不了近渴,iOS 13+ 的最低門檻把國內大多 App 擋在門外,如同以前的 UIStackView 一樣幾年內遙不可及。
UIStackView 呢?
因為去年 App 終於升級了最低支援 iOS 9,所以 安利了一波 UIStackView ,它確實是實現了不少 FlexBox 的功能,但是 StackView 真的是宣告式嗎?
headerStackView.axis = .horizontal
headerStackView.addArrangedSubviews([headerLeftLine,
headerLabel,
headerRightLine])
headerStackView.alignment = .center
headerStackView.snp.makeConstraints {
$0.centerX.equalToSuperview()
}
複製程式碼
只能勉強說有一點宣告式的意思吧。
決定自己封裝
UIStackView 其實足夠強大,問題就出在呼叫層的不夠友好,如果讓它長著 Flutter/Dart 一樣的臉,也許還能一戰。
介紹一下 DeclarativeSugar
直接看效果
和 Flutter 的語法對比
使用 Playground 快速開發
封裝了什麼?
- 宣告式 UI
- 隱藏了 UIStackView 的複雜度和術語
- 支援 UIStackView 的靈活巢狀方式
- 支援 Flutter 的
build()
入口 和更新方法rebuild()
- 支援
Row/Column
,Spacer
(sizedBox
in Flutter) - 支援列表
ListView
(UITableView
in UIKit) - 支援約束
Padding
Center
SizedBox
- 支援手勢
GestureDetector
最低版本: iOS 9
依賴:UIKit
建議使用 Then 來做初始化的語法糖。
這套封裝的另一個目標是減少或者消滅直接使用約束的場景
程式碼結構
安裝
繼承 DeclarativeViewController
或者 DeclarativeView
class ViewController: DeclarativeViewController {
...
}
複製程式碼
重寫 build()
函式,返回你的 UI,和 Flutter 類似。
這個 View 會被加到 ViewController 的 view 上,並且全屏化。
override func build() -> DZWidget {
return ...
}
複製程式碼
功能
1. Row
橫向佈局 同 Flutter 的 Row
DZRow(
mainAxisAlignment: ... // UIStackView.Distribution
crossAxisAlignment: ... // UIStackView.Alignment
children: [
...
])
複製程式碼
2. Column
縱向佈局 同 Flutter 的 Column
DZColumn(
mainAxisAlignment: ... // UIStackView.Distribution
crossAxisAlignment: ... // UIStackView.Alignment
children: [
...
])
複製程式碼
3. Padding
內填充 同 Flutter 的 Padding
3.1 only
DZPadding(
edgeInsets: DZEdgeInsets.only(left: 10, top: 8, right: 10, bottom: 8),
child: UILabel().then { $0.text = "hello world" }
),
複製程式碼
3.2 symmetric
DZPadding(
edgeInsets: DZEdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: UILabel().then { $0.text = "hello world" }
),
複製程式碼
3.3 all
DZPadding(
edgeInsets: DZEdgeInsets.all(16),
child: UILabel().then { $0.text = "hello world" }
),
複製程式碼
4. Center
autolayout 的 centerX 和 centerY
DZCenter(
child: UILabel().then { $0.text = "hello world" }
)
複製程式碼
5. SizedBox
寬高約束
DZSizedBox(
width: 50,
height: 50,
child: UIImageView(image: UIImage(named: "icon"))
)
複製程式碼
6. Spacer
佔位空間
對於 Row
: 同 Flutter 的 SizedBox
設定 width
.
DZRow(
children: [
...
DZSpacer(20),
...
]
)
複製程式碼
對於 Column
: 同 Flutter 的 SizedBox
設定 height
.
DZColumn(
children: [
...
DZSpacer(20),
...
]
)
複製程式碼
7. ListView
列表
隱藏了 delegate/datasource
和 UITableViewCell
的概念
靜態表格
DZListView(
tableView: UITableView().then { $0.separatorStyle = .singleLine },
sections: [
DZSection(
cells: [
DZCell(
widget: ...,
DZCell(
widget: ...,
]),
DZSection(
cells: [
DZCell(widget: ...)
])
])
複製程式碼
動態表格
return DZListView(
tableView: UITableView(),
cells: ["a", "b", "c", "d", "e"].map { model in
DZCell(widget: UILabel().then { $0.text = model })
}
)
複製程式碼
8. Stack
是 Flutter stack, 不是 UIStackView
,用來處理兩個頁面的疊加
DZStack(
edgeInsets: DZEdgeInsets.only(bottom: 40),
direction: .horizontal, // center direction
base: YourViewBelow,
target: YourViewAbove
)
複製程式碼
9. Gesture
支援點選事件(child 是 UIView 呼叫 TapGesture, UIButton 呼叫 touchUpInside)
支援遞迴查詢,也就是說傳入的 child 可以是巢狀很多層的 DZWidget
DZGestureDetector(
onTap: { print("label tapped") },
child: UILabel().then { $0.text = "Darren"}
)
DZGestureDetector(
onTap: { print("button tapped") },
child: UIButton().then {
$0.setTitle("button", for: UIControl.State.normal)
$0.setTitleColor(UIColor.red, for: UIControl.State.normal)
}),
複製程式碼
10. AppBar
支援設定導航欄,這個控制元件只是一個配置類
DZAppBar(
title: "App Bar Title",
child: ...
)
複製程式碼
重新整理
重刷
self.rebuild {
self.hide = !self.hide
}
複製程式碼
增量重新整理
UIView.animate(withDuration: 0.5) {
// incremental reload
self.hide = !self.hide
self.context.setSpacing(self.hide ? 50 : 10, for: self.spacer) // 支援改變區間距離
self.context.setHidden(self.hide, for: self.label) // 支援隱藏
}
複製程式碼
總結
這套輕量封裝已經減輕了不少我日常寫 UI 的認知負擔,提高不少的產能。(程式設計師為了犯懶什麼苦都能吃)
雖然做不到 Flutter 那種 Widget Tree 隨便換,Element Tree 狂優化來兜底,但是對於相對靜態的頁面,佈局變化不大的話,這層封裝還是勝任的。(就是寫法 Fancy 一點的 UITableView/UIStackView
而已)
如果你也覺得有用,歡迎一起來完善。
GitHub 地址: DeclarativeSugar