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

相关文章
相关标签/搜索