前言
自从入坑了 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