Swift 仿 Flutter 風格聲明式 UI 封裝思路

前言

自從入坑了 Flutter,瞭解了現代 web 框架,回頭來看 iOS 原生的命令式 UI 產能實在過低了,就好像騎自行車和汽車賽跑同樣git

問題出在哪?

  1. 沒有響應式,沒有 setState(),這一點能夠經過 RxSwift 的綁定來將就。
  2. 沒有聲明式,傳統的命令式 UI 的代碼和效果不匹配。
  3. 沒有 JIT,編譯耗費大量時間。

命令式的問題在陶文的 面向對象不是銀彈,DDD 也不是,TypeScript 纔是 中有更深刻的討論:程序員

Many states:數量上多
Concurrent / Parallel:併發是邏輯上的,並行是物理上的。不管是哪一種,都比 sequential 更復雜。
Long range causality:長距離的因果關係
Entangled:剪不斷理還亂github

SwiftUI 呢?

雖然 SwiftUI 很美,甚至支持了 Hot reload,可是遠水解不了近渴,iOS 13+ 的最低門檻把國內大多 App 擋在門外,如同之前的 UIStackView 同樣幾年內高不可攀。web

UIStackView 呢?

由於去年 App 終於升級了最低支持 iOS 9,因此 安利了一波 UIStackView ,它確實是實現了很多 FlexBox 的功能,可是 StackView 真的是聲明式嗎?swift

headerStackView.axis = .horizontal
headerStackView.addArrangedSubviews([headerLeftLine,
                                    headerLabel,
                                    headerRightLine])
headerStackView.alignment = .center
headerStackView.snp.makeConstraints {
    $0.centerX.equalToSuperview()
}
複製代碼

只能勉強說有一點聲明式的意思吧。bash

決定本身封裝

UIStackView 其實足夠強大,問題就出在調用層的不夠友好,若是讓它長着 Flutter/Dart 同樣的臉,也許還能一戰。markdown

介紹一下 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 來作初始化的語法糖。
這套封裝的另外一個目標是減小或者消滅直接使用約束的場景app

代碼結構

安裝

繼承 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/datasourceUITableViewCell 的概念

靜態表格

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

相關文章
相關標籤/搜索