![关于SwiftUI,看这一篇就够了](https://static001.infoq.cn/resource/image/88/c8/88a65e08b62f06b9c31457d5bc01b7c8.png)
SwiftUI 是一种新颖的构建 UI 方式和全新的编码风格,本文以通俗易懂的语言,从 Swift 5.1 语法新特性和 SwiftUI 的优势方面进行分享,希望对热爱移动端的同学有一定的帮助,让大家尽可能快速、全面和透彻地理解 SwiftUI。
一、背景
苹果于 2019 年度 WWDC 全球开发者大会上,发布了基于 Swift 建立的声明式框架–SwiftUI,其可以用于 watchOS、tvOS、macOS 等苹果旗下产品的应用开发,统一了苹果平台的 UI 框架。
正如官网所言 Better apps. Less code:用更少的代码构建更好的应用。目前想要体验 SwiftUI,需要以下的准备:Xcode 11 beta 和 macOS Mojave or Higher,如果想要体验实时预览和完整的 Xcode 11 功能,需要 macOS 10.15 beta。
本文主要从以下三个方面讲述 SwiftUI 的特性:
从代码层面理解 Swift 5.1 新语法的底层实现;
从数据流方面阐述 SwiftUI 的黑魔法;
从布局原理层面阐述 SwiftUI 组件化的优势;
二、SwiftUI 的特性
本节对 Opaque Result Type, PropertyDelegate, FunctionBuilder 三个语法新特性进行讲解,结合部分伪代码和数据流分析,由浅入深地理解,其在 SwiftUI 中的作用。
2.1 Opaque Result Type
新建一个 SwiftUI 的新项目,会出现如下代码:一个 Text 展示在 body 中。
对于 some View 的出现,大家可能会觉得很突兀。一般情况下,闭包中返回的类型应该是用来指定 body 的类型,如下代码所示,如果闭包中只有一个 Text,那么 body 的类型应该就是 Text。
然而,很多时候在 UI 布局中是确定不了闭包中的具体类型,有可能是 Text、Button、List 等,为了解决这一问题,就产生了 Opaque Result Type。
其实 View 是 SwiftUI 一个核心的协议,代表了闭包中元素描述。如下代码所示,其是通过一个 associatedtype 修饰的,带有这种修饰的协议不能作为类型来使用,只能作为类型约束来使用。
通过 Some View 的修饰,其向编译器保证:每次闭包中返回的一定是一个确定,而且遵守 View 协议的类型,不要去关心到底是哪种类型。这样的设计,为开发者提供了一个灵活的开发模式,抹掉了具体的类型,不需要修改公共 API 来确定每次闭包的返回类型,也降低了代码书写难度。
2.2 PropertyDelegate
复杂的 UI 结构一直是前端布局的痛点,每次用户交互或者数据发生改变,都需要及时更新 UI,否则会引起某些显示问题。但是,在 SwiftUI 里面,视图中声明的任何状态、内容和布局,源头一旦发生改变,会自动更新视图,因此,只需要一次布局。在属性前面加上 @State 关键词,即可实现每次数据改动,UI 动态更新的效果。
上述代码中,一个 @State 关键词继承了 DynamicViewProperty 和 BindingConvertible,BindingConvertible 是对属性值的绑定,DynamicViewProperty 是动态绑定了 View 和属性。
也就是说,声明一个属性时,SwiftUI 会将当前属性的状态与对应视图的绑定,当属性的状态发生改变的时候,当前视图会销毁以前的状态并及时更新,下面具体分析一下这个过程。一般情况下实现一个 String 属性的初始化,代码如下:
如果代码中有很多这样的属性,而且对某些属性进行特定的处理,上面的写法无疑会产生很多冗余。属性代理(propertyDelegate)的出现就是解决这个问题的,属性代理是一个泛型类型,不同类型的属性都能够通过该属性代理进行特定的处理:
![](https://static001.infoq.cn/resource/image/f5/e4/f5b9dda8374c52bb26ce47422cafd9e4.png)
上述代码的功能如上图所示。通过 @propertyDelegate 的修饰,能够解决不同类型的 value 进行特定的处理;上述包装的方法,能够建立视图与数据之间的关系,并且会判断在属性值发生变化的情况下,通知 SwiftUI 刷新视图,编译器能够为 String 类型的 myValue 生成如下的代码,经过修饰后的代码看起来很简洁。
接下来,我们看一下 @State 的源码:
Swift 5.1 的新特性 Property Wrappers(一种属性装饰语法糖)来修饰 State,内部实现的大概就是在属性 Get、Set 的时候,将部分可复用的代码包装起来,上文中说的“属性代理是一个泛型类型”正能够高效的实现这部分功能。
@State 内部是在 Get 的时候建立数据源与视图的关系,并且返回当前的数据引用,使视图能够获取,在 Set 方法中会监听数据发生变化、会通知 SwiftUI 重新获取视图 body,再通过 Function Builders 方法重构 UI,绘制界面,在绘制过程中会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制,资源浪费。
通过这种编程模式,SwiftUI 帮助开发者建立了各种视图和数据的连接,并且处理两者之间的关系,开发者仅需要关注业务逻辑,其官方的数据结构图如下:
![](https://static001.infoq.cn/resource/image/05/6b/05fb990296a6209f0d1e22bc8dd2eb6b.png)
用户交互过程中,会产生一个用户的 action,从上图可以看出,在 SwiftUI 中数据的流转过程如下:
该行为触发数据改变,并通过 @State 数据源进行包装;
@State 检测到数据变化,触发视图重绘;
SwiftUI 内部按上述所说的逻辑,判断对应视图是否需要更新 UI,最终再次呈现给用户,等待交互;
以上就是 SwiftUI 的交互流程,其每一个节点之间的数据流转都是单向、独立的,无论应用程序的逻辑变得多么复杂,该模式与 Flux 和 Redux 架构的数据模式相类似。
内部由无数这样的单向数据流组合而成,每个数据流都遵循相应的规范,这样开发者在排查问题的时候,不需要再去找所有与该数据相关的界面进行排查,只需要找到相应逻辑的数据流,分析数据在流程中运转是否正常即可。
不同场景中,SwiftUI 提供了不同的关键词,其实现原理上如上文所示:
@State - 视图和数据存在依赖,数据变化要同步到视图;
@Binding - 父子视图直接有数据的依赖,数据变化要同步到父子视图;
@BindableObject - 外部数据结构与 SwiftUI 建立数据存在依赖;
@EnvironmentObject - 跨组件快速访问全局数据源;
以上特性的实现是基于 Swift 的 Combine 框架,下面简单介绍一下。该框架有两个非常重要的概念,观察者模式和响应式编程。
观察者模式是描述一对多关系:一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。这两类对象分别被称为被观察目标和观察者,一个观察目标可以对应多个观察者,观察者可以订阅它们感兴趣的内容,这也就是文中关键词 @State 的实现来源,将属性作为观察目标,观察者是存在该属性的多个 View。
响应式编程的核心是面向异步数据流和变化的,响应式编程将所有事件转成为异步的数据流,更加方便的对这些数据流进行组合变换,最终只需要监听数据流的变化并做出处理即可,因此在 SwiftUI 中处理用户交互和响应等非常简洁。
2.3 FunctionBuilder
在认识 FunctionBuilder 之前,必须先了解一下 ViewBuilder,其是用 @_functionBuilder 来修饰的,编译器会使用。并且对它所包含的方法有一定要求,其隐藏在各个容器类型的最后一个闭包参数中。下面具体介绍所谓的“要求”。
在组合视图中,闭包中会处理大量的 UI 组件,FunctionBuilder 是通过闭包建立样式,将闭包中的 UI 描述传递给专门的构造器,提供了类似 DSL 的开发模式。如下实现一个简单的 View:
查看 HStack 的初始化代码,如下所示:其最后的 content 是用 ViewBuilder 进行修饰的,也就是通过 functionBuilder 对闭包表达式进行了特殊处理,最终构造出视图。
如果没有 FunctionBuilder 这一新特性,那么开发者必须对容器视图进行管理,以 HStack 为例(如下代码所示)。若存在大量的表达式,无疑会让开发者感觉到头疼,而且代码也会很杂乱,结构也不够清晰。
用 @_functionBuilder 修饰的内容,均会实现一个构造器,构造器的功能如上述代码所示。构建器声明几种 buildBlock 方法用来构造视图,这几种方法能够满足各种各样的闭包表达式。下面是 SwiftUI 的 ViewBuilder 几种方法:
上文被 ViewBuilder 修饰的 content,content 在调用的时候,会按照上述合适的 buildBlock 进行构建视图,将闭包中出现的 Text 或者其他的组件 build 成一个 TupleView,并且返回。
但是,@_functionBuilder 也存在一定局限性,ViewBuilder 的 buildBlock 最多传入十个参数,也就是布局中最多只能有十个 View;如果超过十个 View,可以考虑使用 TupleView 来用多元的方式合并 View。
作为 SwiftUI 的新特点之一,FunctionBuilder 倾向于目前流行的编程方式,开发者能够使用基于 DSL 的架构,像 SwiftUI,而不用去考虑具体的实现细节,因为构建器实现的就是一个 DSL 本身。
三、Components
本节通过 DSL 视图的分析,分析 SwfitUI 在布局上的特点,以及利用该特点在组件化过程中的优势。
目前,组件化编程是主流的开发方式,SwfitUI 带来了全新的功能–可以构建可重用的组件,采用了声明式编程思想。将单一、简单的响应视图组合到繁琐、复杂的视图中去,而且在 Apple 的任何平台上都能使用该组件,达到了跨平台(仅限苹果设备)的效果。按照用途大概能够分为基础组件、布局组件和功能组件。
更多的组件详见 example link。
下面以一个 Button 为例子:
其中包含了一个 Button,其父视图是一个 ContenView,其实 ContenView 还会被一个 RootView 包含起来,RootView 是 SwiftUI 在 Window 上创建出来了。通过简单的几行代码,设置了按钮的点击事件,样式和文案。
其视图 DSL 结构如下图所示,SwiftUI 会直接读取 DSL 内部描述信息并收集起来,然后转换成基本的图形单元,最终交给底层 Metal 或 OpenGL 渲染出来。
通过该结构发现,与 UIKit 的布局结构有很大的不同,像按钮的一些属性 background、padding、cornerRadius 等不应该出现在视图主结构中,应该出现在 Button 视图的结构中。
因为,在 SwiftUI 中这些属性的设置在内部都会用一个 View 来承载,然后在布局的时候就会按照上面示例的布局流程,一层层 View 的计算布局下来,这样做的优点是:方便底层在设计渲染函数时更容易做到 monomorphic call,省去无用的分支判断,提高效率。
![](https://static001.infoq.cn/resource/image/41/98/41cabc5ee2823a36093b415a7e8de698.png)
同时 SwiftUI 中也是支持 frame 设定,但也不会像 UIKit 中那样作用于当前元素,在内部也是形成一个虚拟的 View 来承载 frame 设定,在布局过程中进行 frame 计算最终显示出想要的结果。
总之在 SwiftUI 中给一个 View 设置属性,已经不是为当前元素提供约束,而是用一系列容器来包含当前元素,为后续布局计算做准备。
SwiftUI 的界面不再像 UIKit 那样,用 ViewController 承载各种 UIVew 控件,而是一切皆 View,所以可以把 View 切分成各种细致化的组件,然后通过组合的方式拼装成最终的界面,这种视图的拼装方式提高了界面开发的灵活性和复用性。因此,视图组件化是 SwiftUI 很大的亮点。
四、See it live in Xcode
SwiftUI 的 Preview 是 Apple 的一大突破,类似 RN、Flutter 的 Hot Reloading。Apple 选择了直接在 macOS 上进行渲染,不过需要搭载有 SwiftUI.framework 的 macOS 10.15 才能够看到 Xcode Previews 界面。
Xcode 将对代码进行静态分析 (得益于 SwiftSyntax 框架),找到所有遵守 PreviewProvider 协议的类型进行预览渲染。在 Xcode 11 中提供了实时预览和静态预览两项功能,实时预览:代码的修改能够实时呈现在 Xcode 的预览窗口中;此外,Xcdoe 还提供了快捷功能,通过 command+鼠标点击组件,可以快速、方便地添加组件和设置组件属性。
五、畅想
SwiftUI 不仅为 Apple 的平台带来了一种新的构建 UI 的方式,还有全新的 Swift 编码风格;
可以推断出:SwiftUI 会出现很多组件库,方便前端开发;
支持热更新,这一点可能让更多的开发者拥抱 SwiftUI;
虽然 SwiftUI 优点很多,但是其使用的门槛很高,只能在 iOS 13 以上的系统使用;仅这点,很多公司和开发者望而却步,目前主流应用最低支持 iOS 9,至少 3 年之内,SwiftUI 只能作为一个理论的知识储备,所以其还有很长的路要走;
SwiftUI 这种与平台无关、纯描述的 UI 框架,恰恰是跨平台方案的正确方向,将来其能否统一整个大前端呢?这点非常值得期待;
作者介绍:
梁启健,携程金融支付中心开发工程师,主要负责支付 iOS 端的开发与优化工作,喜欢研究大前端和跨平台技术。
本文转载自公众号携程技术中心(ID:ctriptech)。
原文链接:
评论