时光荏苒,SwiftUI 技术已经推出一年,从 WWDC 2020 来看,SwiftUI 团队付出了空前的努力,使得 SwiftUI 无论是在开发体验,还是性能上都得到了很大的提升。如果说 SwiftUI 是去年苹果在开发技术转型上的小试牛刀,那么今年的 SwiftUI 基本已经成为了未来 5-10 年苹果生态开发技术的主流方式。
众所周知, SwiftUI 是一种数据驱动型的 DSL。也就是说,所有的界面显示效果,必须去改变数据然后通过绑定才能体现到相应的视图上。那么接下来,设计和组织好这些数据结构,才能让 SwiftUI 发挥出它的特性和优势。
WWDC 2019 Data Flow Through SwiftUI[1] 对应的文章 SwiftUI 数据流[2] 有幸也是笔者操刀撰写,文章内介绍了SwiftUI 数据流的基本原理、绘制流程以及发展趋势,并且对比了各种不同的编程范式,感兴趣的读者可以回顾阅读。
去年 SwiftUI 给出了 @State, @ObservedObject,@EnvironmentObject[3] 三个 PropertyWrapper 来处理视图和数据之间的绑定依赖关系,但并没有详细的讲解这几个 PropertyWrapper 的使用场景以及区别,后续大家都是根据自己的编码和调试经验得出一些使用上的技巧,今年苹果直接拿出一个 Book Club App[4] 做为案例,全方位的给你讲解如何使用它们,并给出一些参考规范,真香!本 Session 是由 Curt Clifton[5] 、Luca[6]、Raj Ramamurthy 三位大神为我们讲解,议题主要围绕以下三个方面:
从本质上讲,编写 SwiftUI 应用程序还是属于前端技术的范畴,所以写界面是每位开发者都应该掌握的技能,那么当大家拿到设计稿开始编码的时候,首先要思考以下三个问题:
那么现在我们用上面这个设计图来回答以上三个问题:
通过回答上面问题,很容易就可以编写上述代码,可以看出 BookCard 视图的数据是由 Book 这个结构体提供的,Book 数据结构中包含了书籍的封面、标题、作者等信息,Progress 用来标记读书的进度;这个书籍列表视图只是用来展示数据,所以 book 和 progress 都用 let 声明为常量。那这些数据从哪来?他们可以通过在实例化 BookCard 的时候,通过构造函数从父视图传进来,每次渲染调用 body 计算属性获取绘制信息时,BookCard 都会实例一次。这个新实例化的 BookCard 生命周期只局限于本次渲染,如果下次渲染流程里它不再需要展示,那么它就被销毁了。它的可视化视图层级以及对应的数据关系图,如下:
接下来我们进入书籍详情页面,设计图如下,当点击 Update Progress 按钮的时候,需要弹出一个弹层,来记录读书进度。
根据信息展示的对应关系,我们需要一个 Boolean 变量 isEditorPresented 来控制弹层的显示与否,需要一个 String 类型的 note 来记录备注信息,需要一个 Double 类型的 progress 来记录读书进度,代码如下:
通常情况下可以把这些数据组装到一个 EditorConfig 的结构体中,如下图:
这样组装成 EditorConfig 后,BookCard 代码一下就清晰了,也非常方便进行独立测试。由于 EditorConfig 是一个值类型,改变它的内部属性它本身也会发生改变。
以上我们处理完弹层视图与数据之间的信息展示对应关系,那么接下来处理第二个问题 - “视图和数据之间的操作逻辑有哪些“,当我们要展示弹层的时候,我们需要将 isEditorPresented 属性设置为 true,那么就需要给 EditorConfig 添加一个 mutating 方法来进行更改并且初始化一些其他信息,如下图:
最后来处理第三个问题 - ”数据由谁持有“,从代码看 EditorConfig 上的数据都是 BookView 视图本地持有,没有从父视图传递进来,再结合上面数据需要被更改的情况,这时候需要创建一个单一数据源以维持数据与视图之间的同步。在 SwiftUI 中创建单一数据源最简单的方法就是 @State Property Wrapper,用 @State 来修饰属性后,SwiftUI 便接管了被修饰属性的存储 (简单理解就是 Get, Set 方法)。那么 SwiftUI 为什么要这么处理呢?因为如果是一个单纯的结构体,每次调用 body 进行重绘的时候,都是实例化一个全新 EditorConfig 结构体,对于只是展示数据的视图是没问题的,但是如果视图中有修改数据的行为,那么被修改的数据在结构体被销毁的时候也丢失了,而 SwIftUI 通过 @State PropertyWrapper,帮我们缓存住 EditorConfig,每次绘制的时候从内存缓存的数据中进行恢复。
那接下来让我们再用三步走法则,思考下 ProgressEditor (弹层视图) 该怎么实现,在这里值得注意的是弹层的数据从哪来的,由谁持有?如果我们先假设 ProgressEditor (弹层视图) 自己持有 EditorConfig,直接在它内部声明为一个属性,然后在实例化的时候从 BookView 传递进来,这样做只是传递了一份数据拷贝,当 ProgressEditor (弹层视图) 要修改 EditorConfig 的值时,也只是对自己内部的拷贝进行更改,不会影响到 BookView 中的 EditorConfig 实例,所以两边的数据是不同步的。那么我们给 ProgressEditor (弹层视图) 的 EditorConfig 也添加上一个 @State PropertyWrapper 可以吗?答案是否定的,这样做相当于在 BookView 和 ProgressEditor (弹层视图) 里创建了两个数据源,但是我们需要一个数据源来保持两个视图的数据同步,在 SwiftUI 中这种共享数据源的方式可以用 @Binding PropertyWrapper 来实现。使用 @Binding 后,现在 SwiftUI 帮你接管 EditorConfig 而且 BookView 和 ProgressEditor 都依赖于这个共享数据源。所以当数据发生变化时,SwiftUI 会对两个视图进行重绘。
接下来 ProgressEditor 还需要把修改好的数据传递给 BookView 进行展示,SwiftUI 采用了 PropertyWrapper 的 Projection 特性来实现了双向数据绑定,所以只需要在参数前面加一个 $ 符号即可。最终相当于 ProgressEditor (弹层视图) 直接跟 BookView 里的 EditorConfig 数据建立依赖关系。很多 SwiftUI 默认控件也采用的了这种绑定声明机制。
@State 主要是针对视图内短暂的状态处理,但是通常情况下,UI 代码和业务逻辑代码是分开的。这个时候想要把业务逻辑代码绑定到 SwiftUI 视图中,就需要用到 ObservableObject。首先我们来看下ObservableObject 协议是如何定义的:
我们可以把 ObservableObject 理解为视图和数据建立依赖关系的中间层(类似 ViewModel),ObservableObject 内部不仅仅包含要展示到视图上的数据,也可以处理业务逻辑,数据缓存,网络请求等操作。当然你可以根据自己的业务逻辑来定义 ObservableObject 的生命周期。比如可以将所有的数据都包含在一个 ObservableObject 中,所有的视图都通过这个 ObservableObject 与相对应的数据建立依赖关系,如下图:
当你的数据结构非常复杂的时候,也可以拆分多个 ObservableObject 分别管理对应的视图数据,处理起来非常灵活性,如下图:
接下来,我们结合 Book App,看下代码是如何编写的:
我们这里定义了一个 CurrentlyReading 类来处理当前阅读书籍的业务逻辑,它内部的 Book 数据是不变的,可以用 let 声明,当我们要更新阅读进度时,我们直接对 @Published 修饰的属性进行更改,SwiftUI 通过之前对该属性的监听,就能感知到数据变化,触发 SwiftUI 重绘,然后在各 View 节点获取到定义的视图结构信息,组装成渲染树,最终将数据的变化呈现到新渲染的视图上,所以 @Published 就是 SwiftUI 感知 ObservableObject 数据变化的标记,它有以下特性:
上面讲解了 ObservableObject 中间层如何定义,那么如何绑定到视图呢?SwiftUI 提供了三个 PropertyWrapper (属性修饰器):
下面给出一些案例代码的使用场景:
上面我们已经了解到如何为 SwiftUI 定义好数据中间层,接下来讨论下如何让 SwiftUI App 有更好的性能。首先我们深入思考下 View 到底是什么?
View 其实就是一部分 UI 属性信息的定义,SwiftUI 根据这些定义好的属性信息来进行渲染绘制,同时帮你标记好不同的 View 以管理他们的生命周期。由于 View 只是一些基本视图信息的定义,所以它非常轻量而且廉价。在 SwiftUI App 中所有界面上展示的视图都是一个 View。这里要注意的是 View 的生命周期和定义它的结构体的生命周期是不一样的,遵循了 View 协议的结构体,生命周期非常短,用完即销毁。也就是意味着 SwiftUI 只通过每个结构体的 body 属性来获取绘制信息,然后在内部对这些信息做一次拷贝,这时遵循 View 协议的结构体已经没用了,就会被销毁掉。SwiftUI 内部根据拷贝过来的信息进行绘制处理,当然通过信息计算后,如果有些 View 不需要显示,也会被被销毁掉。
我们可以通过下图,了解到基本的渲染流程,当视图上有事件触发后,会对本地的数据进行更改,这个数据更改会影响到数据源的更改,当数据源发生了变化,SwiftUI 会触发重绘,重新获取新的视图定义结构,然后内部渲染成新的 UI 视图呈现给用户。
为了更好的理解,我们可以把上面的渲染流程简化成一个简单的圆环,如下图:
每次渲染的过程这个圆环就会重复执行相关操作。所以如何维持这个圆环执行流畅是保证 SwiftUI App性能的关键。如果圆环流程卡住了,SwiftUI 通常称这种现象为 Slow Update(慢更新),出现这种情况就说明你的 App 有丢帧的情况 。
那么如何避免上述情况呢?下面给出三点注意:
接下来,我们通过下面 Book Club App 中的代码来看一个 SlowUpdate 的示例:
这里注意在 ReadingList 中定义的 ObservedObject 数据,看起来定义挺合理的,其实是有个 Bug 的,每当 ReadingListViewer 被创建的时候,ReadingListStore 都会再被实例化一次,因为每次 SwiftUI 都会把 ReadingList 实例化一次,进行拷贝。这样就导致了 SlowUpdate 的出现,同时也会导致数据的丢失。那么怎么解决呢?按照去年的实现标准,需要把 store 放到父视图中定义,然后用参数把它传递进来。但能不能能保持这种内部的编写方式呢?这样做更容易分割组件。今年新推出的 @StateObject PropertyWrapper[8] 就可以解决这个问题。就像上面提到的,StateObject PropertyWrapper 告诉 SwiftUI 在合适的时机再去实例化 ObservableObject,相当于在内部做了缓存机制,这样数据不会丢失,同时也避免了不必要的实例化。
在这里要注意的是,SwiftUI 不仅要响应界面操作事件,还要处理很多其他的外部通知事件,所以今年提供了一些新事件处理的 Modifier,如下图,这些 Modifier 可能会用一个 closure 回调来处理操作,但回调函数是在 UI 线程上执行的,如果要做逻辑复杂的处理,建议单独在后台线程执行。
我们再回到三步走法则,仔细想想数据由谁持这个问题,其实这个问题是最难回答的,因为它没有一个标准答案。在这里只能给出一些参考场景:
由于今年 SwiftUI 不仅只有 View 这一种视图,还增加了 Scene 和 App,所以数据的生命周期,也可以根据这些不同的视图定义发生变化,在 Scene 中你可以为每个 Window 定义一个全局的数据源,这样不同 Window 之间的数据操作就会相互隔离,如下图:
当然你可以用用 @StateObject 为 App 定义一个 App 级别的全局数据源,如下图:
到目前为止,虽然我们可以处理各种数据与视图之间的依赖关系,但是每当 App 重新后,内存数据也就不存在了,这种情况通常我们要自己处理缓存逻辑,今年 SwiftUI 新增了本地缓存机制的 PropertyWrapper。它们拓展了数据的生命周期,而且可以自动从本地缓存中恢复过来:
上述文章中介绍了基本上所有 SwiftUI PropertyWrapper[9],笔者总体汇总下各自的使用场景,方便大家记忆和区分:
通过上述对比,推荐下我的编码流程,通常情况下,我先不会管这些属性修饰器,上来我会先定义视图对应的数据结构,直接在 SwiftUI 里面用上,等处理到相关 View 和数据绑定的时候,再回过头来加这些属性修饰器,或者更改一些数据结构的定义方式,相反如果一上来就考虑这些属性修饰器该怎么用,个人感觉很容易就陷入无限的思考,导致自己无法下手写代码,大家可以做为参考。
本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/smQmdXF-3VkHQnUV3G8Z3w
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。