声明式 UIKit 在有赞美业的实践

云栖号资讯:【点击查看更多行业资讯】
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!

一、背景

随着 Flutter 的出现,UI 开发形式也越来g J g x :越趋向相同,Flutter,SwiftUI,RN,Weex 等新兴 UI 框架无一意N - N 2 i ?外都使用了声明式的 UI 开发模式,h k 2 K 0和支持了 FlexBox 的布局系统G { t A

FlexBox 自从被 W3C 提出之后,就被前端] J R d O . J发扬光大,所有浏览器都已经得到了支持。其方便的布局方式,通过布局来接管视图的大小和位置,使得各个视图节点得到了很好的解耦,大大地提高了 UI 代码的可移植性。

在 iOS 的布局方式里,除了极少数或者对性能要求极高的地方会用 frh n u Same 进行直接计算赋值外,大多数情况下是使用到 UIKit 提供的 Autolayout 进行布局。它相对于 frame 计算来说,通过对 view 之间的关系进行7 f ~ | n s m U描述,来达到布局的效果。由于提供的是约束,强调视图“之间”的相C J P V 4互关系,t p ] i I + N 4意味着关系] h D一旦定下来,修改约束(包括添加e q [ b和移除视图),必定会h f m - 2 & u z ;影响到另一个视图的引用,从而导致Q . B #代码移植性不高。同时布局是通过每一行代码的描述来约定与其他视图之间的布局关系,在修改 UI 之前,出了需要看明白视图x u k v l创建流程,还要熟读每一行的约束,了解清楚它们之间的关系才能修改。

在 iOS 9 之后,UIKy w I R F $it 提供了 UIStackView 就是通过类似 FlexBox 的形式,接管视图之间的布局规则,减少对视图的的操作,来达到快速布局的效果。

K 7 I 2们会发现,跨平台以及原生端包括 iOS 和安卓,都会对 FlexBox 布局多少有点涉足,在现在大前端的开发趋势下,统一的布局方式和思路显得尤为重要。

当位置和大小被布局接管后,视图之间的依赖没有了,转化为添加视图的顺序和各自的属性,d y ?会发现声明式的 API 在布局系统中能非常好地契合。

  • 代码结构O ( b k x即 UI 结构
  • UI 节点相互独立,可移植性高
  • 布局规则与 UI 视图分离,h L 6 S : ! A c高度灵活可定制

二、实现

想要实现 UIKit 使/ x ] n p a用声明式的布局方式,首先要解决布局问题。现在开源的一些第三方库其实已经有很好地解决。比如:ComponentKit,Yoga,AsynDisplayKit 等,都是基于布局的设计。他们都有一个很明显的特点,就是都使用了 C++ 进行, w a算法编写。我们知道,布局进行抽象后,其实就是对节点的位置和大小的计算,与视图没有任何的关系,基于这个问题,W u ] b 1 d其实算法是可以抽离的,使用 C++ 不仅性能高,还可以跨平台。

然而有赞美业是有赞最早迁移到 Swift 的项目,迁移 Swift 后的代码,对 C++ 的兼容其实有一定的局限性,若过多使用 C++,在代码中也不能体现 Swift 的优雅性。

同时上述开源v 5 E Q库中,只有 Compo{ # s - [ SnenR / x rtKit 实现了声明式设计,OC 中繁琐的中括号和声明式多级的缩进结合起来,代码的可读性也大打折扣L P 0 y q z 0 ! 5

SwiftUI 由于 iOS 13 的限制,以及生态的不完善,也还没达到我们在项目中能直b J K |接上手h # I x $ 7使用的程度,前期版本的迭代也会因为其不稳定等因素可能会出现 API 的大改。

基于上述问题,我们决定自己封装基于 Swift 的声明式 UI 开发框架。

2.1 APx q A h g UI 设计

UIView().attach($0) {
UILabel().~ + sattach($0)
.text(" 测试 Label")
UIButton().attach($0)
.text("x S [ 3 O T a 4 8 测试 Button", state: .normal)
}

基于 Swift 简洁的语法,! p 0 Z x ? y 4 X我们可以V ~ G 2很简单地设计出基W 3 ~ ? = I [ # h于结构化的 API 设计^ X l | 5 {

一眼就能看出其 UI 结构。

UIView
|-- UILabel
|-- UIButton

同时 View 之间没有相互联系,移植性高。

为什么不通过描述对象的方式来声明而采用直接使用 View 进行声明呢?

这里主要是因为 UIKit 绘制是需要通过 UIView 和它的 layer 进行的。而它里面具备着大量的属性以及方法,如N g A J 2 _果全部都需要接管的话,无疑会是一个工作量非常大的动作。对于1 D F 9 j C 7 ` g我们来说成本时极高的,而我们的目- * f k的是使用布局2 h 7 [ )和声明式来提高开发效率。

2.2 布局设计

由于我们的结构和 API 设计是基于 UIView 的方式,布局在设计中,其实也是一个 View,一个具备布局能力的视图容器。布局能力我们是可以进行抽象的,即布局的算法

针对布局的算法,我们抽象出可被计算节点4 r H [ Z H U % W Me? w ,asure,以及可计算节点 Regulator。抽象节点和 View tree 将一1 M M E ; (一对应,分别描述的是普通节点以及布局容器。继承关g T I 3 l | ?系如下:

声明式 UIKit 在有赞美业的实践

在节点中,容器其实也是其中的一个视图节点,因此也具备可被计算能力。并且 Regulatorx t h可以横向扩展,支持更多样化的布局规则。

同时 Measurable 为一个接口,具备可被计算能力。

public protocol Measurable {
func caculate(byParent pary b P 1ent:s ` X ? Measu0 W _ !re, remain size: CGSize) -> Size
}

每个布局视图都持有相对应的Regulator ,在layoutSubviews时进行布局的计算。

声明式 UIKit 在有赞美业的实践

通过布局和普通视图的声明,可以高效地绘制是目标 UI。

attach {
HBox().r 9 ( @ O F M rattach($0) {
UIImageView().attach($0)
.size(80, 80)
.cornerRadius(6)
VBox().i d q 1attacG N 3 M / t . i 8h($0) {
UILabel().attach($0)
.text("name")
UILe 0 3 n Iabel().attach($0)
.text("description")
}
.format(.center)
.width(.fill)
.Q P %space(10)
}
.space(10)
.padding(all: 16( K t 6 ])
.width(.fill)
.cornerRadil b t k C K B ` wus(6M 8 %)
}

声明式 UIKit 在有赞美业的实践

2.3 数据交互设计

UIKit 本身设计为 MVC 模式,以命令的形式对 UI 进行修改等T J e 8 `操作。这就需要我们在所有响应操作(比如点击,网络回& O T 9调)之后,获取到相应的 View 进行修改。额外的使用` ? { C H n f C变量获取 view,不管是对代码4 6 s的额外增量以及内存泄露的风险都是存在的。

从上面代码来看,View 完全可以在声明期间操作,之后可以不持有任何的 view,通过父 View 的 subviews 进行持有管理,从而确保整个 view 树在一个( K ( [ C N根节点下B G o W | & R挂载。一旦上级 view 释放,子节点将跟随释放,降低内存泄露风险。

计算机的用户界面开发1 T ( S { j K的目的,其实就是解决人与机器之间的D ) ) M ] g输入和输出操作,通过点击滑动等操作将事件进行输入,通过 UI 状态的改变将信息输出。

我们可以理解为,在声} ` { O F T 8明 UI 过程中,需要给 View 提供输入以及输出接口,通过绑定接口来实现事件的回调和响应。

为此我们引入了 State ,通过 View 在声明过程中与 State 进行绑定。

/// 输出接口
public protocol Outputing {
associatQ b { ( - }edtype OutputType
func outputing(_ / R E U C { / O block: @escaping (OutputT, W 6 y + b c T Sype) -> Void) -> Unbinder
}
/// 输入接口
public protocol Inputing {
associatedtype InputType
func input(value: InputType)
}
public class State<Value>: Outputing5 s q, Inputinr L H A 5 [ 4 Pg {
...
}

通过声明State来进行状态绑定:

letT 3 4 + K g x d ) statk J h Le = State<String>("")
attach {
UILabel().attach($0)
.text(state)
UIButton().attach($0)
.bind(event: .touchUpInside, input: SimpleInput { _ in
print("touched")
})
}
// 当事件回调
state.value = "new value"

通过状态管理,UI 代码和数据操作将天然地分离,State 最小的状态管理节点,可基于 Redux 等状态管理模式统一管理,也可以分散各自 ViewController 进行分离式管理。| Y q }取决于当前项目的状态管理机制。

2.4 动画处m M ` q

Flutter 等通过描述语言进行构建 UI 的方式,是使用不可变的节点 进行 View Tree 的描述,在 State 变动的时候根据位置信息实时重新 rebu. R i F u ( s Q Sild ,因为 Flutter 内部有强大的 Rel] j ~ ^ 5 % Y Rayoutboundary 进行性能控制,重X n @ = e * 6 V新创建的 Widget 也只是描述信息,消耗较小。相比于用 UIView 直接声明结构的形式,View 的创建是非常重的,重复创建不现实。在 U# # 7 I % % L X HIView tree 中,View 是可变的,只需要把变化的操作放入动画 block 中即可由系统完成。

VBox().attach {
UILabel().attach($0)
}
.animator(Animators.default)
// 在布局重新计算子 view 时,根据动画对象进行
func layoutSubviews() {
su8 M } Z 8 Kper.layoe ( b ^ |utSubviews()
ani, | V - !mator.animate {
self.caculateChildren()
}
}
// 在设置完约束直接获取到 view
UIView.animate(0.2) {
view.lm  o B F 2 K QayoutIfNeeded()
}

2.5 数据驱动

在原生 APP 开发的时候,除了我们常见的 View 叠加,还有一个重要的组成部分,列表。UIKit 中的列表 UITableView, UICollecU b / 2 GtionView 在日常开发中扮演者重要的角色。通过不同的 id 区分不同类型的 Cell,以及高效的回收机制,为复杂列表开q ? 5 @ # F e } 2发提供稳定的性能支撑。在声明式和响应式数据交互的加持下,我们可以通过响应数据的变化,把 TableView,CollectionView 的 MVC 设计模式,修改为响应式。因为驱动 View 变化的只有纯数据源,同时可以通过数据源变化提供 difI B ; ` Sf 计算,高效计算需要重绘的节点。

let dataSource = State<[String]>([])
attach {
TableBox(
style: .plain,
sections: [
TableSection<String, UIView, Void&W . z ^ / H l `gt;(
dataSource: dataSource,
differ: { $0 },
_cell: { output, i C a E ] k ( 9nput in
Cell().attach()
.viewState(output.map { $0.data })
.view
}
)
]
)
.at8 + @ : Y M e 8 *tach($0)b 7  K J X 7 x I
.size(.fill, .fill)
}

2.6 样式表

在项目中对于 View 样式的统一风格,传统的方式更多% 3 e s Q是使用继承,基类配置基础样式,子类再次扩展新样式。这样处理往往导致一条条很长的继承链,过度的自定义会导致代码复^ j R y 3 j用性降低。

为了解决这个问题,在样式上处理上,我们导入了 Style 概念,和 CSSa / J F l ( E ~ K 一样,Style 是对样式的描述,调用方可以任意组合 Style,把样式和从 UI 代码中抽离,能够很好地减少自定义 vB E # k 2iew 的继承和实现,通过组合的形式,提高代码的复用和S 2 [ R l S @灵活性。

let styles = [
TapRippleSh c # ` jtyle(), // 具备点击涟漪效果
TapScaleStyle(), // 具备点击放大缩小的效果
(UIView.backgroundColor).getStyle(va{ U  V + : `lue: .black) // 背景色黑色
]
attach {
UIView().attach($0)
.styles(styles)
}

三、实践

在美业 n W * o的改版项目中全面投入使用了该框架开发后,明显得到以下几点的收益:

  • UI 开发转向了纯响应式

通过输入和输出的定义,最大限度地保证了数据流的单向性,复杂的 View 状态管理被转换成数据的管理。

  • 开发效率提升

由于 API 的限制,团队成h l x N U /员的开发模式被统一,大大降低跨业务开发和接受他人业务模块难度。

  • 代码量降低,灵活度更高

布局 Box 和样式的加成,让继承体系: 9 A ? : . _转换为组合形式,灵活组装,维护成本降低。非复用性自定义 view 数量降低。

但也由于其开发模式和 MVC 大相径庭,导致开发成员在初期使用的时候也会走不少的弯路,对布局系统的不熟悉,以及布局方式的陌生,对于前期的上手U - r来说是有点难度。但是在声明式大潮流的方向下,这些都是我们必须去适应的。

四、未来

虽然现在暂时实现了我们所需要的功能,但依旧是建立在 UIKit 之上的,声明的并不是 UI 描述,而是直接的 UI 树,这意味Q T J着我们的任何一个布局都是一层 View,无可避免地会在一定程度上加重了我| Z ^ k们的 View 层级。

后续我们所需要D # C X f o G解决的问题:

  • 通过标记(dirty)以及 Boundary,减少重复计算。
  • 约束布局(ConstraintBox)。
  • 通过虚拟布局(ViewGroup)等概念,减少 View 层级,提升渲染性能。

基于声明式 UIKit 的分享到n c N G q * T P这里就结束了,在未来我们也会不断地优化这个方案,在 UI 开发/ [ e $ % I层面上达到代码更优雅,可读性更高,性能更优。

【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/live

立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https( Y ] R 7 X { + h://c.tb.cn/F3.Z8gvnK

原文发布时间:2020-06-16
本文作者:王俊仁
本文来自:“infoq”,了解相关信息可以关注“infoq”