【AICon】探索八个行业创新案例,教你在教育、金融、医疗、法律等领域实践大模型技术! >>> 了解详情
写点什么

关于 SwiftUI,看这一篇就够了

  • 2019-09-30
  • 本文字数:6589 字

    阅读完需:约 22 分钟

关于SwiftUI,看这一篇就够了

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 中。


struct ContentView : View {       var body: some View {             Text("Hello World")       } }
复制代码


对于 some View 的出现,大家可能会觉得很突兀。一般情况下,闭包中返回的类型应该是用来指定 body 的类型,如下代码所示,如果闭包中只有一个 Text,那么 body 的类型应该就是 Text。


struct ContentView : View {       var body: Text {             Text("Hello World")       } }
复制代码


然而,很多时候在 UI 布局中是确定不了闭包中的具体类型,有可能是 Text、Button、List 等,为了解决这一问题,就产生了 Opaque Result Type。


其实 View 是 SwiftUI 一个核心的协议,代表了闭包中元素描述。如下代码所示,其是通过一个 associatedtype 修饰的,带有这种修饰的协议不能作为类型来使用,只能作为类型约束来使用。


通过 Some View 的修饰,其向编译器保证:每次闭包中返回的一定是一个确定,而且遵守 View 协议的类型,不要去关心到底是哪种类型。这样的设计,为开发者提供了一个灵活的开发模式,抹掉了具体的类型,不需要修改公共 API 来确定每次闭包的返回类型,也降低了代码书写难度。


public protocol View : _View {        associatedtype Body : View        var body: Self.Body { get } }
复制代码

2.2 PropertyDelegate

复杂的 UI 结构一直是前端布局的痛点,每次用户交互或者数据发生改变,都需要及时更新 UI,否则会引起某些显示问题。但是,在 SwiftUI 里面,视图中声明的任何状态、内容和布局,源头一旦发生改变,会自动更新视图,因此,只需要一次布局。在属性前面加上 @State 关键词,即可实现每次数据改动,UI 动态更新的效果。


@propertyDelegate public struct State<Value> : DynamicViewProperty, BindingConvertible 
复制代码


上述代码中,一个 @State 关键词继承了 DynamicViewProperty 和 BindingConvertible,BindingConvertible 是对属性值的绑定,DynamicViewProperty 是动态绑定了 View 和属性。


也就是说,声明一个属性时,SwiftUI 会将当前属性的状态与对应视图的绑定,当属性的状态发生改变的时候,当前视图会销毁以前的状态并及时更新,下面具体分析一下这个过程。一般情况下实现一个 String 属性的初始化,代码如下:


public struct MyValue {     var myValueStorage: String? = nil

public var myValue: String { get { myValue = myValueStorage return myValueStorage } set { myValueStorage = newValue } } }
复制代码


如果代码中有很多这样的属性,而且对某些属性进行特定的处理,上面的写法无疑会产生很多冗余。属性代理(propertyDelegate)的出现就是解决这个问题的,属性代理是一个泛型类型,不同类型的属性都能够通过该属性代理进行特定的处理:


@propertyDelegate public struct LateInitialized<Value> {  private var storage: Value?    public init() {    storage = nil  }    public var value: Value {    get{      guard let value = storage       createDependency(view, value) // 建立视图与数据依赖关系      return value    }    set {      if(storage != newValue){        storage = newValue        notify(to: swiftui) // 通知 SwiftUI 数据有变化      }    }  }}
复制代码



上述代码的功能如上图所示。通过 @propertyDelegate 的修饰,能够解决不同类型的 value 进行特定的处理;上述包装的方法,能够建立视图与数据之间的关系,并且会判断在属性值发生变化的情况下,通知 SwiftUI 刷新视图,编译器能够为 String 类型的 myValue 生成如下的代码,经过修饰后的代码看起来很简洁。


public struct MyValue {  var $myValue: LateInitialized<String> = LateInitialized<String>()
public var myValue: String { get { $myValue } set { $myValue.value = newValue} }}
复制代码


接下来,我们看一下 @State 的源码:


@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)@propertyDelegate public struct State<Value> : DynamicViewProperty, BindingConvertible {
/// Initialize with the provided initial value. public init(initialValue value: Value)
/// The current state value. public var value: Value { get nonmutating set }
/// Returns a binding referencing the state value. public var binding: Binding<Value> { get }
/// Produces the binding referencing this state value public var delegateValue: Binding<Value> { get }
/// Produces the binding referencing this state value /// TODO: old name for storageValue, to be removed public var storageValue: Binding<Value> { get }}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)extension State where Value : ExpressibleByNilLiteral {
/// Initialize with a nil initial value. @inlinable public init()}
复制代码


Swift 5.1 的新特性 Property Wrappers(一种属性装饰语法糖)来修饰 State,内部实现的大概就是在属性 Get、Set 的时候,将部分可复用的代码包装起来,上文中说的“属性代理是一个泛型类型”正能够高效的实现这部分功能。


@State 内部是在 Get 的时候建立数据源与视图的关系,并且返回当前的数据引用,使视图能够获取,在 Set 方法中会监听数据发生变化、会通知 SwiftUI 重新获取视图 body,再通过 Function Builders 方法重构 UI,绘制界面,在绘制过程中会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制,资源浪费。


通过这种编程模式,SwiftUI 帮助开发者建立了各种视图和数据的连接,并且处理两者之间的关系,开发者仅需要关注业务逻辑,其官方的数据结构图如下:



用户交互过程中,会产生一个用户的 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:


struct RowCell : View {    let image : UIImage    let title : String    let tip : String        var body: some View {        HStack{            Image(uiImage: image)            Text(title)            Text(tip)        }    }}
复制代码


查看 HStack 的初始化代码,如下所示:其最后的 content 是用 ViewBuilder 进行修饰的,也就是通过 functionBuilder 对闭包表达式进行了特殊处理,最终构造出视图。


init(alignment: VerticalAlignment = .center, spacing: Length? = nil, @ViewBuilder content: () -> Content)
复制代码


如果没有 FunctionBuilder 这一新特性,那么开发者必须对容器视图进行管理,以 HStack 为例(如下代码所示)。若存在大量的表达式,无疑会让开发者感觉到头疼,而且代码也会很杂乱,结构也不够清晰。


struct RowCell : View {    let image : UIImage    let title : String    let tip : String        var body: some View {        var builder = HStackBuilder()        builder.add(Image(uiImage: image))        builder.add(Text(title))        builder.add(Text(tip))        return builder.build()    }}
复制代码


用 @_functionBuilder 修饰的内容,均会实现一个构造器,构造器的功能如上述代码所示。构建器声明几种 buildBlock 方法用来构造视图,这几种方法能够满足各种各样的闭包表达式。下面是 SwiftUI 的 ViewBuilder 几种方法:


Building Blocksstatic func buildBlock() -> EmptyView//Builds an empty view from a block containing no statements.
static func buildBlock<Content>(Content) -> Content//Passes a single view written as a child view through unmodified.
static func buildBlock<C0, C1>(C0, C1) -> TupleView<(C0, C1)>static func buildBlock<C0, C1, C2>(C0, C1, C2) -> TupleView<(C0, C1, C2)>static func buildBlock<C0, C1, C2, C3>(C0, C1, C2, C3) -> TupleView<(C0, C1, C2, C3)>...
复制代码


上文被 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 为例子:


struct ContentView : View {    var body: some View {        Button(action: {            // did tap        },label: {Text("Click me")}        )        .foregroundColor(Color.white)        .cornerRadius(5)        .padding(20)        .background(Color.blue)    }}
复制代码


其中包含了一个 Button,其父视图是一个 ContenView,其实 ContenView 还会被一个 RootView 包含起来,RootView 是 SwiftUI 在 Window 上创建出来了。通过简单的几行代码,设置了按钮的点击事件,样式和文案。


其视图 DSL 结构如下图所示,SwiftUI 会直接读取 DSL 内部描述信息并收集起来,然后转换成基本的图形单元,最终交给底层 Metal 或 OpenGL 渲染出来。


通过该结构发现,与 UIKit 的布局结构有很大的不同,像按钮的一些属性 background、padding、cornerRadius 等不应该出现在视图主结构中,应该出现在 Button 视图的结构中。


因为,在 SwiftUI 中这些属性的设置在内部都会用一个 View 来承载,然后在布局的时候就会按照上面示例的布局流程,一层层 View 的计算布局下来,这样做的优点是:方便底层在设计渲染函数时更容易做到 monomorphic call,省去无用的分支判断,提高效率。



同时 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)


原文链接


https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&mid=2697268883&idx=2&sn=98725f332e8c091726fbb549e25fb669&chksm=8376f1a7b40178b16f1a1d78103de64066212463da50a9cb52ed76a624dc3ff71a369cdca56a&scene=27#wechat_redirect


2019-09-30 08:0012248

评论

发布
暂无评论
发现更多内容

数据结构与算法-时间和空间复杂度

Byte_Panda

算法

欢迎来到机器人的打工时代「幻想短篇 6/28」

道伟

28天写作

项目管理系列(2)-如何写好一份报告

Ian哥

项目管理 28天写作

大流量场景下如何云淡风轻地进行线上发布?

阿里巴巴中间件

Swift 算法-栈

Byte_Panda

算法

读书笔记:《中产阶级如何保护自己的财富》

lidaobing

28天写作 中产阶级如何保护财富

生产环境全链路压测建设历程 28:FAQ 之 混沌工程

数列科技杨德华

28天写作

读《快手要上市了》,一起了解快手

李忠良

开源 技术 28天写作

碎碎念之「被误会的佛系,被遗忘的疯魔」

Justin

碎碎念 心灵鸡汤 28天写作 佛教

精选算法面试-数组(二分查找)

李孟聊AI

面试 算法 数组 28天写作

城市生态的机器人革命

脑极体

关于“面试造火箭,入职拧螺丝” Jan 14, 2021

王泰

28天写作

微信视频号的排版,怎样才好看 | 视频号 28 天 (07)

赵新龙

28天写作

【TF2系列笔记】Day01:在VSCode中创建开发环境

IT蜗壳-Tango

七日更 TF2

Spring 源码学习 14:initApplicationEventMulticaster、onRefresh 和 registerListeners

程序员小航

spring 源码 源码阅读

创业失败启示录|校园微生活(故事篇3)

阿萌

28天写作 创业失败启示录 青城

pub哥的2020文章清单

JavaPub

Java javapub

两种常见的减少信息不对称的办法

熊斌

学习 成长 28天写作

测试一年多,上线就崩溃!微服务到底应该怎么测试?

阿里巴巴中间件

中间件

【计算机内功修炼】五:从小白到高手,你需要理解同步与异步

码农的荒岛求生

异步 同步 回调函数

28 天带你玩转 Kubernetes-- 第六天(玩转 Docker命令)

Java全栈封神

Docker k8s 28天写作 docker命令

[4/28]保障产品高质量交付业务价值

L3C老司机

《适用于初学者的Python》

计算机与AI

简单三招,每个管理者都可以成为有温度的共情高手

一笑

沟通与管理 28天写作

为什么我们需要自动化回归?

阿里巴巴中间件

中间件

HDFS SHELL详解(7)

罗小龙

hadoop 28天写作 hdfs shell

醒醒!Python已经支持中文变量名啦!

Python猫

Python

LeetCode题解:105. 从前序与中序遍历序列构造二叉树,递归+数组切割,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

油车和电车比到底哪个整体能源利用效率高?(28天写作 Day6/28)

mtfelix

自动驾驶 28天写作 电动汽车

28天瞎写的第二百一七天:你们 CentOS 服务器还有图形界面啊?

树上

28天写作

关于焦虑的思考

.

28天写作

关于SwiftUI,看这一篇就够了_语言 & 开发_梁启健_InfoQ精选文章