SwiftUI

Swift 仿 Flutter 风格声明式 UI 封装思路

2019-08-09  本文已影响4人  30505346a4cb

前言

自从入坑了 Flutter,了解了现代 web 框架,回头来看 iOS 原生的命令式 UI 产能实在太低了,就好像骑自行车和汽车赛跑一样

问题出在哪?

  1. 没有响应式,没有 setState(),这一点可以通过 RxSwift 的绑定来将就。
  2. 没有声明式,传统的命令式 UI 的代码和效果不匹配。
  3. 没有 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

直接看效果

[图片上传中...(image-b7d339-1565360981222-3)]

<figcaption></figcaption>

和 Flutter 的语法对比

[图片上传中...(image-dc8daf-1565360981222-2)]

<figcaption></figcaption>

使用 Playground 快速开发

[图片上传中...(image-cbf0c0-1565360981222-1)]

<figcaption></figcaption>

封装了什么?

最低版本: iOS 9
依赖:UIKit

建议使用 Then 来做初始化的语法糖。
这套封装的另一个目标是减少或者消灭直接使用约束的场景

代码结构

[图片上传中...(image-953eca-1565360981222-0)]

<figcaption></figcaption>

安装

继承 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 而已)

如果你也觉得有用,欢迎一起来完善。

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:810733363。不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。
来源:本文为第三方转载,如有侵权请联系小编删除。

上一篇 下一篇

猜你喜欢

热点阅读