大多数浏览器和
Developer App 均支持流媒体播放。
-
SwiftUI 中的数据要素
对任何 app 来说数据都是一个复杂的部分,但是 SwiftUI 从原型到生产都可以确保一个平稳、数据驱动的体验。了解 State 和Binding 这两个功能强大的工具,它们可以保留和无缝更新你的真值来源。我们还将向你展示 ObservableObject 是如何让你将视图连接到数据模型的。你可以直接通过专家了解一些棘手的挑战和解决这些问题的好方法! 为了充分利用本节内容,你需要熟悉 SwiftUI。 请观看“SwiftUI 中的 app 必备知识”和“SwiftUI 介绍”。
资源
相关视频
WWDC22
WWDC21
WWDC20
-
下载
(你好 WWDC 2020) 你好 欢迎来到 WWDC
你好 欢迎来到 SwiftUI 的数据要素 我是 Curt Clifton SwiftUI 团队的工程师 稍后 我的朋友和同事 Luca Bernardi 和 Raj Ramamurthy 也将加入我的行列 在这次演讲中 我们将涉及三个主要领域 我将讨论如何开始处理 SwiftUI app 中的数据流 并涵盖状态和绑定等主题 然后 Luca 将讨论如何 在这些想法的基础上 为你的 app 设计数据模型 最后 Raj 将分享如何将数据模型集成到 你的 app 中的技术 在此过程中 我们将讨论 SwiftUI 视图的生命周期 并分享一些非常酷的新功能 这些功能让你建模数据变得更加容易 我们还将帮助你更深入地理解 Swift 中的值和引用类型 如何与 SwiftUI 中的数据流交互 我将介绍 SwiftUI 中的 一些基本数据流特性 并介绍一些在创建一个 新的 SwiftUI 视图时 你应该问自己的问题 Luca 非常喜欢读书俱乐部 他非常喜欢这些活动 于是决定开发一款 app 来记录他的每个俱乐部正在读什么 并为下次的会议做些笔记 我也喜欢阅读 我觉得一起开发一个 app 会很有趣
首先 我想创建一个视图原型来表示 我正在阅读的一本书 当我在 SwiftUI 中打开一个新视图时 我要考虑三个关键问题 这个视图需要什么数据来完成它的工作? 在这个例子中 视图需要一个 图书封面的缩略图 包括书名、作者的名字 和我读过这本书的百分比 视图将如何处理该数据? 该视图仅需要显示数据 它不会改变它 数据从哪里来? 这就是“真相的来源” 在整个演讲的例子中 Luca、Raj 和我 将用这个徽章来标记真相的来源
最终 正如我们将看到的 真相的来源问题 是数据模型设计中最重要的一个问题 让我们建立这个视图 看看它的真相来源应该是什么
那么这个视图需要什么数据 来完成它的工作呢? 图书值提供封面图片的名称 作者和标题 进度值提供完成百分比 视图如何处理这些数据? 它只是显示数据 而不是改变数据 所以这些是“let”属性
数据从何而来? 当 BookCard 被实例化时 它将从超级视图中传递 真相的来源在视图层次结构的更上层 当 BookCard 实例化时 超级视图将真实数据传递进来 每次超级视图的主体执行是 一个新的 BookCard 被实例化 实例存在的时间足够让 SwiftUI 渲染它 然后它就消失了 我们可以用图表把内部结构可视化 左边的框表示用于构建 此 BookCard 的 SwiftUI 视图 右边的胶囊表示 用于呈现视图的数据 在整个演讲中 我们将返回到类似的图表 (远大的前程) 接下来我想看一个稍微更有趣的观点 当我轻点一本书时 这个 app 会把我带到这个屏幕上 在这里我可以回顾我的进度 我想添加一种更新进度的方法 并在我点击“更新进度”按钮时添加注释 像是这样的 (更新进度) 让我们看看父视图和这个工作表 是如何一起工作的
我们将专注于如何呈现表格 当用户点击时“更新进度”按钮时 我们将调用 presentEditor 方法 它将改变某些状态使工作表出现
该视图需要什么数据控制工作表的显示? 我们需要一个布尔值来跟踪 是否提供工作表 我们需要一个字符串来跟踪笔记 和一个双精度浮点数来跟踪进度 每当我有像这样的多个相关属性时 我喜欢将它们抽出到它们自己的结构体中
除了使 BookView 更具可读性之外 这种方法还有另外两个好处 我们得到了封装的所有好处 editorConfig 可以在其属性上 维护不变量 并独立地进行测试 而且因为 editorConfig 是一个值类型 所以对 editorConfig 属性的任何更改 如其进程 都可以作为对 editorConfig 本身的 更改可见 我们的下一个问题是 视图将如何操作这些数据? 当点击“更新”按钮时 我们需要将 isEditorPresented 设置为“True” 并更新进度以匹配当前进度
因为我们将状态提取到 editorConfig 结构中 所以我们可以让它负责这些更新 并让 BookView 请求 editorConfig 来完成这些工作 然后 我们将添加一个变异方法到 editorConfig 像这样
数据从哪里来? 编辑器配置对这个视图来说是本地的 没有一些父视图可以传递它 所以我们需要建立一个本地的真相来源 在 SwiftUI 中 真相最简单的来源 就是状态
当我们将该属性标记为 State 时 SwiftUI 将接管对其存储容量的管理 我们用图表来看一下 同样 左边的这些框表示视图 右边的数据由一个胶囊表示 请注意胶囊的边界很厚 我用这个边界来表示 SwiftUI 为我们管理的数据 为什么这很重要? 请记住 我们的视图只是暂时存在的 在 SwiftUI 完成一个渲染后 结构本身就会消失 但是因为我们将这个属性标记为 State 所以 SwiftUI 为我们维护它 下次框架需要呈现此视图时 它将重新实例化结构并将其 重新连接到现有存储容量
接下来让我们看看 ProgressEditor ProgressEditor 需要哪些数据 来完成它的工作? 所有数据都来自 editorConfig 以及视图如何操作这些数据? 它需要改变它 所以我们将使用一个变量 数据从哪里来? 在这种情况下是一个有趣的问题 回到图表 让我们关注一下 BookView 和 ProgressEditor 假设我们只是将 editorConfig 作为常规属性 向下传递给 ProgressEditor 因为 editorConfig 是值类型 Swift 会复制该值 ProgressEditor 对备忘录或进程 所做的任何更改 只会改变这个新副本 而不会改变 SwiftUI 为我们管理的 原始值 所以这就不允许 ProgressEditor 与 BookView 通信 接下来是什么? 如果我们让 ProgressEditor 拥有自己的 State 属性如何? 这似乎就是我们想要的 它告诉 SwiftUI 为我们 管理一个新的数据
可悲的是 这也是错误的 记住 State 创造了一个新的真相来源 ProgressEditor 所做的任何更改 都将被修改到它自己的状态 而不是与 BookView 共享的状态 我们需要唯一的真相来源 我们需要一种与 ProgressEditor 共享 从 BookView 对真相源 进行 write-access 的方法 在 SwiftUI 中 用于共享 对任何真相来源的 write-access 的工具是 Binding BookView 创建绑定到 editorConfig 对现有数据的 read-write 引用 并与 ProgressEditor 共享该引用
通过这个 Binding 来更新 editorConfig ProgressEditor 就会改变 BookView 正在使用的相同状态 SwiftUI 会注意到对 editorConfig 的更改 它知道 BookView 和 ProgressEditor 都依赖于那个值 所以它知道当值改变时 重新呈现那些视图
The ProgressEditor 需要将数据 交回 BookView
我们刚刚看到 Binding 是完成这项任务的合适工具
调用中的美元符号从 State 创建一个 Binding 因为 State 属性包装器的预计价值 是一个 Binding Binding 属性包装器 在 BookView 中的 ProgressEditor 和 editorConfig 状态之间 创建一个数据依赖项 许多内置的 SwiftUI 控件 也可以接收 Binding 例如 用于备忘录的新 TextEditor 控件 接收到一个将 Binding 绑定到 String 的值 使用美元符号预计价值访问者 我们可以在 editorConfig 中获得 备忘录的新 Binding SwiftUI 让我们可以从现有的 Binding 构建新的 Binding 记住 Binding 不仅仅是为了 State
这是关于在 SwiftUI 中 开始使用数据流的一点内容 在添加 SwiftUI 视图时 记得问自己这三个问题 该视图需要什么数据? 它将如何使用这些数据? 数据从何而来?
你可以为没有改变的数据使用属性 将 State 用于视图拥有的临时数据 使用 Binding 来改变另一个视图 拥有的数据
现在我想把话题交给 Luca 让他来深入讲解设计你的数据模型 因为 State 并不是全部 谢谢 Curt 大家好 我是 Luca 我的同事 Curt 刚刚描述了 如何使用 State 和 Binding 来驱动你用户界面中的更改 以及这些工具是如何成为 快速迭代你的视图代码的好方法 但 State 是为视图本地的 瞬时用户界面状态设计的 对视图而言是本地的 在本节中 我将把你的注意力 转移到模型的设计上 并解释 SwiftUI 提供给你的所有工具 通常 在你的 app 中 你通过使用 一个独立于用户界面的数据模型 来存储和处理数据 这时就到了需要管理数据生命周期的 关键时刻 包括持久化和同步 处理副作用 更常见的是 将其与现有组件集成 这时你应该使用 ObservableObject 首先 让我们看看 ObservableObject 是如何定义的 它是一个类约束协议 这意味着它只能被引用类型采用 它有一个单一的需求 一个 objectWillChange 属性 ObjectWillChange 是一个发布者 顾名思义 ObservableObject 协议的语义要求 是发布者必须在对对象 应用任何突变之前发出 默认情况下 你可以获得一个 开箱即用的优秀发布者 但是如果你需要 你可以提供自定义发布者 例如 你可以使用发布者作为计时器 或者使用观察现有模型的 KVO 发布者
现在我们已经了解了协议 让我们看看用于理解 ObservableObject 的心理模型 当你的类型符合 ObservableObject 时 你正在创造新的真相来源 并教会 SwiftUI 如何对变化做出反应 换句话说 你定义了视图渲染其用户界面 和执行其逻辑所需的数据
SwiftUI 将在你的数据和你的视图之间 建立一个依赖关系 这里用蓝色框表示 SwiftUI 使用这种依赖关系 来自动保持视图的一致性 并显示数据的正确表示
我们喜欢将 ObservableObject 看作是 数据依赖面 这是模型中向视图公开数据的部分 但不一定是完整的模型
如果你以这种方式 来考虑 ObservableObject 那么它不一定就是你的数据模型 你可以将数据与其存储容量 和生命周期分离 你可以使用值类型建模数据 并使用引用类型管理其生命周期和副作用 例如 你可以有一个 ObservableObject 它集中了整个数据模型 并由你的所有视图共享 如图所示 这为所有逻辑提供了一个单独的位置 便于推断在你的 app 中 所有可能的状态和突变 或者 你可以通过在数据模型上提供 特定投影的多个 ObservableObject 来关注你的 app 的一部分 这些被设计为仅公开所需的数据 当你有一个复杂的数据模型 并且你想为你的 app 的一部分 提供一个更紧密的失效范围时 这种方式更好 让我们回到我们正在构建的 app 看看如何应用我们刚刚讨论的内容 让我们回到 Curt 之前处理的视图 这种观点让我可以更新自己在书中的进度 并在阅读过程中添加一些有趣的备忘录
Curt 关注的是进度表的数据 让我们看看这个视图需要的其他数据 以及如何使用 ObservableObject 添加丰富的特性 比如将数据存储在磁盘上 或者与 iCloud 同步
我们可以创建一个名为 CurrentlyReading 的新类 该类符合 ObservableObject 它存储一本书及其阅读进度 这是我们将向视图公开的模型的一部分 用于显示数据和响应更改 特定的书永远不会改变 所以我们可以将这个属性设置为 let 我们希望能够更新进程 并让用户界面对其做出反应 我们可以通过使用 @Published 属性包装器 对进度属性进行注释来实现这一点 让我们看看 @Published 是如何工作的 @Published 是一个属性包装器 它通过公开发布者 使属性可观察 对于大多数模型 你只需遵从 ObservableObject 添加 @Published 到可能会改变的属性中 就可以了 很简单 永远不必担心 保持数据与视图同步 当你将 @Published 与默认的 ObservableObject 发布者 一起使用时 系统将在值更改之前 通过发布自动执行无效 对于高级用例 @Published 的预计值是一个发布者 你可以使用它来构建反应流
让我们回顾一下在设计数据模型时 应该问的三个问题 用 ObservableObject 来回答第二个问题就很简单了 我们总是假设可变性 现在让我们来看看如何回答其他两个 关于 ObservableObject 的问题 我们已经了解了如何定义模型 但是现在需要在视图中使用它 SwiftUI 提供了三个属性包装器 你可以在视图中使用它们 来创建对 ObservableObject 的依赖关系 这些工具是 ObservedObject 今年新推出的 StateObject 和 EnvironmentObject 让我们先看一下 ObservedObject ObservedObject 是一个属性包装器 你可以使用它来注释 持有符合 ObservableObject 类型的 视图的属性 使用它可以通知 SwiftUI 开始跟踪 视图的依赖属性 你正在定义此视图执行其工作所需的数据
这是最简约和最灵活的工具 ObservedObject 不会获得你提供给它的 实例的所有权 管理它的生命周期是你的责任 让我们看看如何在 app 中使用它 这是我的 BookView 这个视图显示了我的书以及到目前为止 我所记录的所有进展 从这个屏幕 我也可以更新这本书的进度 我们可以添加一个属性 到 currentlyReading 这是我们刚刚创建的类型 并使用 @ObservedObject 属性包装器 对其进行注释 分配给这个属性的实例 将是这个视图的真实来源 现在我们可以在视图主体中读取模型 SwiftUI 将保证视图永远是最新的 你不需要写代码 也不需要事件来保持你的视图同步 你只需要声明性地描述 如何从数据中编写视图 然后 SwiftUI 会处理所有剩下的工作 我的同事 Raj 会详细介绍 SwiftUI 更新的生命周期 但我想带大家看看 SwiftUI 为你做了什么 无论何时你使用 ObservedObject SwiftUI 都会订阅 那个特定的 ObservableObject 的 objectWillChange 现在 每当 ObservableObject 改变时 所有依赖于它的视图都会被更新 我们经常遇到的一个问题是 “为什么‘在将来’改变? 为什么不是 ‘在过去’ 改变?” 原因是 SwiftUI 需要知道 什么时候会发生变化 这样它就可以将每一个变化 合并为一个更新 但是我们如何产生突变呢? SwiftUI 提供的很多用户界面组件 都有 Binding 到数据的功能 在之前的演讲中 我们已经看到了一个如何设计组件 来执行相同操作的例子 这是 SwiftUI 的基本设计原理之一 接受 Binding 允许组件对数据 进行读写访问 同时保留唯一的真相源
Curt 展示了如何从 State 派生 Binding 对 ObservableObject 做同样的事情 也很简单
你可以从任何属性获得一个 Binding 这在 ObservableObject 上是值类型的 只要在变量前面加上美元符号前缀 并访问属性就行了 让我们看一个具体的例子 在我们的读书俱乐部 app 里 阅读一本书的最大满足之一是 完成最后一个篇章 我想要一个大动作来标记这个事件 我想不出什么比轻击一个切换开关 更能说明我已经读完这本书了 首先我需要改变的是我的模型 我们可以添加一个新属性“isFinished” 并用 @Published 注释 这样每当它的值改变时 我们的用户界面将反映出这一变化 让我们看看如何改变 BookView 来添加这个特性 我可以添加一个带有漂亮标签的切换开关 来标记完成了这本书 切换开关期待一个 Binding 这样就可以显示当前状态 当用户轻击它时 它可以改变值 我们可以提供“isFinished”的 Binding 只需添加美元符号前缀 到 currentlyReading 并访问 “ isFinished” 属性 现在 每当用户与切换开关交互时 CurrentlyReading 将被更新 而 SwiftUI 将更新所有依赖它的视图 例如 在这个视图中 当我们读完这本书时 我们将禁用按钮来更新进度 在整个我们的 app 中 我们看到这些漂亮的书封面 我们希望在它们显示在屏幕上之前 从网络异步加载它们 它们是昂贵的资源 所以我们只希望在视图可见的时候 让它们存活 普遍来说 我们注意到你经常 想把你的 ObservableObject 的生命周期 和你的视图联系起来 就像 State 一样 如果你还记得 ObservedObject 并不拥有它的 ObservableObject 的生命周期 所以我们想提供一个更符合 人体工程学的工具
这个工具是今年新推出的 StateObject 当你用 StateObject 注释一个属性时 你会提供一个初始值 而 SwiftUI 会在第一次运行主体之前 实例化这个值 SwiftUI 将在视图的整个生命周期中 保持对象的活动状态 这是对 SwiftUI 的一个很好的补充 让我们看看如何在实践中使用它 让我们创建一个 CoverImageLoader 类 来按名称加载特定的图像 这里我只是展示了这个类的框架 而不是我如何使它成为一个 ObservableObject 以及如何用 @Published 注释 保存图像的属性 一旦我们有了图像 SwiftUI 就会自动更新视图 现在在我们的视图中 我们可以使用 CoverImageLoader 并用 StateObject 注释属性 CoverImageLoader 将不会 创建视图时实例化 相反 它将在主体运行之前被实例化 并在整个视图生命周期中保持活动状态 当不再需要视图时 SwiftUI 会像预期的那样释放 CoverImageLoader 你不再需要摆弄 onDisappear 了 相反 你可以使用 ObservableObject 终生管理你的资源 在 SwiftUI 中 视图非常便宜 我们鼓励你将它们作为首要的封装机制 并创建易于理解和重用的小视图 这意味着你最终可能会得到 一个像图中所示的 深视图层次结构 如果我们从数据流的角度来看这个问题 那么可能需要在视图层次结构的高处 创建一个 ObservableObject 并将相同的实例传递给多个视图 有时 你需要在一个遥远的子视图中 使用 ObservableObject 并将其传递到不需要数据的视图中 这样会很麻烦 还会涉及到很多样板文件 幸运的是 我们有一个解决方案 针对 EnvironmentObject 的此问题 EnvironmentObject 既是视图修饰符 又是属性包装器 在你想注入 ObservableObject 的 父视图中 使用视图修饰符 并且在希望读取特定 ObservableObject 实例的 所有视图中使用属性包装器 框架将负责将该值传递到 需要它的任何地方 并仅在读取它的地方作为依赖项追踪它 在演讲的这一部分中 我们了解了 如何使用 ObservableObject 来设计你的模型 以及 SwiftUI 如何提供了 所有的基本图元 你可以使用这些元素来构建 最适合你需求的架构
我们看到了如何创建一个视图 和一块数据之间的依赖 如何使用新的 StateObject 属性包装器 将 ObservableObject 的生命周期 绑定到视图 以及最后如何使用 EnvironmentObject 为你提供方便 以便 ObservableObject 流到视图层次结构中 现在我要把话题交给 Raj 他将深入介绍 SwiftUI 视图的生命周期 以及它与性能的关系 如何处理副作用 以及一些令人兴奋的新工具 谢谢大家 希望大家有一个很棒的 WWDC 是吧 Raj? 谢谢 Luca 你已经学习了如何在 SwiftUI 中 开始使用数据 以及如何设计一个很棒的模型 现在 我将告诉你如何为最佳性能 进行设计 以及在选择真相来源时的一些进一步考虑 首先 我们来谈谈视图 及它们在 SwiftUI 系统中的角色 视图只是一块用户界面的定义 SwiftUI 使用这个定义来创建合适的渲染 SwiftUI 管理你的视图的身份和寿命 由于视图定义了一块用户界面 所以它们应该是快捷的、便宜的 在读书俱乐部 app 中 与每个 app 一样 屏幕上的所有内容都是一个视图 (标题视图 - 进度视图 封面视图) 请注意 视图的生命周期 与定义它的结构的生命周期是分开的 你创建的符合 View 协议的结构 实际上有一个非常短的生命周期 SwiftUI 用它来创建渲染 然后渲染消失了 让我们用一个图表来说明 使视图快捷的重要性
该图显示了 SwiftUI 的更新生命周期 从顶部开始 我们有你的用户界面 逆时针方向移动 会导致一些 闭包或操作运行的事件 这就导致了真相来源的突变
一旦我们改变了真相的来源 我们将得到一个新的视图副本 我们将使用它来生成渲染 那个渲染就是你的用户界面 这只是基础 我们稍后会再看一下这个图 现在我们来简化一下 再多一点
好多了 顾名思义 这个生命周期是一个周期 当你的 app 运行时 它会不断重复很多次 这个周期对性能也非常重要
理想情况下 这个周期会像这样 平稳地进行
但如果在这些点上有昂贵的阻塞工作 你的 app 的性能就会受到影响
我们称之为缓慢更新 这意味着你可能会删除帧 或者你的 app 可能会挂起 (昂贵的工作导致更新缓慢) 那么 如何避免更新缓慢? 好吧 避免更新缓慢你要做的第一件事 是确保你的视图初始化很便宜 一个视图的主体应该是一个 没有副作用的纯函数 这意味着你应该简单地 创建视图描述并返回 不派遣其他工作 只需要创建一些视图 然后继续 避免对主体被调用的时间和频率进行假设 请记住 SwiftUI 有时候是非常聪明的 这意味着它可能不符合你的假设
为了说明主体便宜的重要性 让我们看一个例子
下面是一个简单的例子 说明我们当前的阅读列表可能是什么样的 在 ReadingListViewer 中 我们将主体定义为 包含 ReadingList 的 NavigationView 然后 在 ReadingList 我们将使用 一个 ObservedObject 来生成数据
这似乎很合理 但实际上这里潜伏着一个错误 你能看见它吗?
事实证明 每当 ReadingListViewer 的主体被创建 都将导致 ReadingListStore 的 重复堆分配 这会导致更新缓慢 记得之前你的视图结构 没有一个定义的生命周期 所以当你写这个的时候 你实际上每次都要创建一个对象的新副本 这样的重复堆分配既昂贵又缓慢 它还会导致数据丢失 因为该对象每次都会重置 好的 我们已经确定了一个问题 但是我们如何解决呢? 在过去 你必须在其他地方 创建模型并传递它 但如果你能内联实现此步 岂不是很棒? 我很高兴地告诉你 今年我们有了一个 叫做 StateObject 的工具 来解决这个问题 正如 Luca 先前解释的那样 StateObject 让 SwiftUI 在正确的时间 实例化你的可观察对象 这样你就不会产生不必要的堆分配 也不会丢失数据
这意味着你可以获得良好的性能和正确性 有一件事需要注意 我们在这里宣布了一个新的真相来源 所以让我们给它贴上图标吧 好 我们再看一遍图表 以及增强 太好了 需要注意的一点是 左上角有多个事件来源 你的 app 可能会对用户交互产生的事件 做出反应 比如轻击一个按钮 或者它可能会对发布者产生反应 比如计时器或通知 不管触发事件的是什么 周期都是一样的 我们将改变真相的来源 更新视图 然后生成一个新的渲染 这些触发器称为事件源 他们是改变你视图的驱动力 如前所述 这方面的一个很好的例子 是用户交互 或发布者 比如计时器 今年 我们有了新的方法来响应事件 比如环境和绑定的变化 以及处理 URL 和用户活动的新方法 这些修饰符中的每一个都接受一个参数 比如一个发布者或一个要比较的值 并且还接受一个闭包 SwiftUI 将在正确的时间关闭 帮助你避免缓慢的更新 保持主体是便宜的 但是请注意 SwiftUI 会在主线程上 运行这些闭包 所以如果你需要做昂贵的工作 可以考虑将闭包发送到后台队列 我们今天不打算详细讨论这些问题 但我鼓励您查看说明文档 以进一步了解更多信息 以上是关于如何避免缓慢更新 以及如何设计出高性能的指南 接下来 让我们回到这三个问题
我们已经大致讲了如何解决前两个问题 但是第三个问题就不那么简单了 当你回答这个问题时 你正在决定将要使用的真相来源 这是一个很难的问题 这里没有一个正确的答案 所以为了帮助大家 我想介绍一些考虑因素
要问的一个重要问题是 谁拥有这些数据? 它是使用数据的视图 还是那个视图的一个祖先? 也许它甚至是你 app 的另一个子系统
这里有一些指导方针 如前所述 与一个共同的祖先共享数据 对于你的 ObservableObjects 也更喜欢将 StateObject 作为视图拥有的真相来源 最后 考虑在 app 中放置全局数据 我们接下来会讨论这个问题
现在让我们谈谈数据的生命周期 我们将讨论如何将数据生命周期 与 Views 、Scenes 和 Apps 联系起来 更多关于 SwiftUI Scenes 和 Apps 的信息 请参考 “SwiftUI 中的 App 要点”会话 首先 让我们从 Views 开始
正如我们在整个演讲中向你展示的那样 视图是一个很好的工具 可以将你的数据生命周期绑定 并且我们今天讨论的所有属性包装器 都与视图一起工作 你可以使用 State 和 StateObject 属性包装器 将数据生命周期与视图生命周期联系起来 接下来 让我们谈谈场景 因为 SwiftUI 的每个场景 都有一个独特的视图树 你可以把重要的数据挂在树的根上 例如 你可以在窗口组内的视图中 放置一个真相源
这样做时 窗口组创建的场景的每个实例 都可以拥有自己的、独立的真实来源 作为底层数据模型的一个镜头 它与多个窗口一起使用时效果很好
如你所见 这两个场景具有独立的用户界面状态 它们表示相同的底层数据 所以现在 Luca 可以同时 参加两个在线读书俱乐部 接下来 让我们讨论一下 App App 是 SwiftUI 今年推出的 一个强大的新工具 它让你可以只用 SwiftUI 来编写你的整个 app 但 App 的好处在于你可以在 app 中 使用 State 和其他真相来源 就像在视图中一样 让我给你看一个例子 这里 我们将使用 StateObject 属性包装器 为整个 app 创建一个全局图书模型 每当模型改变时 我们所有 app 的场景 因此 他们的所有视图 都会更新 通过写这个 我们创建了一个 app-wide 真相来源 当考虑把你的真相来源放在哪里时 你可能会想把它放在 app 中 如果它代表真正的全球数据 你的数据生命周期很重要 但是这个生命周期与它的真实来源的 生命周期相关联 在前面的所有示例中 我们使用了 State、StateObject 和 Constants 等工具 但这些有一个限制 它们与进程的生命周期绑定 这意味着如果你的 app 被杀死 或设备重启 状态将不会恢复
为了帮助解决这个新问题 今年我们引入了存储容量
这些具有延长的使用寿命 并自动保存和还原 注意 这些不是你的模型 相反 它们是与模型一起使用的快捷存储
让我们从场景 - 存储容量开始
场景 - 存储容量 是一个每个场景范围的属性包装器 它完全由 SwiftUI 管理读写数据 它只能从 Views 内访问 这使得它非常适合存储 关于您的 app 当前状态的快捷信息 让我们看一个例子
这是之前的读书俱乐部 app 有两个并列的场景 在使用场景 - 存储容量时 首先要考虑的是 你真正需要还原的是什么? 在这种情况下 我们只需要保存 然后选择还原 书名、进度和备忘录都存储在模型中 所以我们不需要保存和还原它们 让我们跳到代码 在这里你可以看到我们如何使用场景 - 存储容量来保存和还原我们选择的状态 我们将传递一个密钥 针对我们要存储的 数据类型 该密钥必须唯一 然后 我们可以像 State 一样使用它 SwiftUI 将自动保存并代表我们还原价值
由于它的行为类似于 State 我们实际上已经宣布这里是新的真相来源 而这次 这是整个场景的真相来源 现在 如果设备重启 或系统需要回收资源支持一个场景 这些数据将在下一次发布时可用 让 Luca 可以从他上次离开的地方 开始他的读书俱乐部活动 现在你已经了解了场景 - 存储容量的 实际应用 接下来我将讨论 App - 存储容量 这是 app 范围的全局存储 使用户默认值持久化 它可以在任何地方使用 所有你可以从你的 app 或视图中访问它 App - 存储容量 像用户默认设置一样 对于存储少量数据非常有用 例如设置 在我们的读书俱乐部 app 中 我们还决定引入一些设置 让我们看看如何使用 App - 存储容量 来实现这一点 在我们的设置视图中 我们需要做的就是添加 App - 存储容量 属性包装器并给它一个密匙 默认情况下 它使用标准用户默认值 但如果需要 你可以自定义它 有关更多信息 请参阅说明文档 在本例中 我们将为这两种设置 分别设置一个 App - 存储容量 我们将分别给它们一个唯一的密钥 因为它们是独立的数据 在使用场景 - 存储容量 和 App - 存储容量时 独特的数据块具有独特的密匙是很重要的 就像其他属性包装器一样 这也是真相的来源 这意味着我们可以绑定到它 比如在切换开关中使用 现在 无论何时数据发生变化 它都会自动保存并从用户默认值中还原 今天 我们已经看到了用于流程生存周期 和扩展生命周期的各种工具 你可以在此处使用每种工具来存储 用户界面级别的状态 对于存储类型 请注意 不要使用它们存储所有内容 因为持久性不是免费的 我还想提出另一个工具 那就是 ObservableObject 我们将 ObservableObject 设计得非常灵活 所以你可以使用它来实现超级自定义行为 比如通过服务器或其他服务来备份数据 这些工具组成了一个家族 让你可以高度自由地选择 最适合你的需求的工具 我们今天已经看到了很多工具 和它们的用法 但主要的结论是没有一种万能的工具 每个 app 都有自己的一套特性 所以一个典型的 app 会使用各种工具 我们已经在我们的 读书俱乐部 app 中做到了这一点 我们用 State 来表示按钮 和我们的进度视图 用 StateObject 来表示当前读取的模型 用 Scene-Storage 表示我们的选择 以及用 App-Storage 表示我们的设置 重要的是要考虑你的数据属性是什么 应该使用什么正确的真相来源 你还应该尝试限制真相来源的数量 以减少复杂性 最后 利用绑定 作为构建可重用组件的一种方式 请记住 绑定对其真相的来源 是完全不可知的 这使它们成为构建 整洁的抽象化的强大工具 现在你已经了解了要点 是时候建立一个很好的模型了 谢谢
祝你有一个很棒的 WWDC
(你好 WWDC 2020)
-
-
2:09 - BookCard
struct BookCard : View { let book: Book let progress: Double var body: some View { HStack { Cover(book.coverName) VStack(alignment: .leading) { TitleText(book.title) AuthorText(book.author) } Spacer() RingProgressView(value: progress) } } }
-
3:35 - EditorConfig
struct EditorConfig { var isEditorPresented = false var note = "" var progress: Double = 0 mutating func present(initialProgress: Double) { progress = initialProgress note = "" isEditorPresented = true } } struct BookView: View { @State private var editorConfig = EditorConfig() func presentEditor() { editorConfig.present(…) } var body: some View { … Button(action: presentEditor) { … } … } }
-
5:59 - ProgressEditor
struct EditorConfig { var isEditorPresented = false var note = "" var progress: Double = 0 } struct BookView: View { @State private var editorConfig = EditorConfig() var body: some View { … ProgressEditor(editorConfig: $editorConfig) … } } struct ProgressEditor: View { @Binding var editorConfig: EditorConfig … TextEditor($editorConfig.note) … }
-
13:15 - CurrentlyReading
/// The current reading progress for a specific book. class CurrentlyReading: ObservableObject { let book: Book @Published var progress: ReadingProgress // … } struct ReadingProgress { struct Entry : Identifiable { let id: UUID let progress: Double let time: Date let note: String? } var entries: [Entry] }
-
15:36 - BookView
struct BookView: View { @ObservedObject var currentlyReading: CurrentlyReading var body: some View { VStack { BookCard( currentlyReading: currentlyReading) //… ProgressDetailsList( progress: currentlyReading.progress) } } }
-
17:50 - CurrentlyReading with isFinished
class CurrentlyReading: ObservableObject { let book: Book @Published var progress = ReadingProgress() @Published var isFinished = false var currentProgress: Double { isFinished ? 1.0 : progress.progress } }
-
18:21 - BookView with Toggle
struct BookView: View { @ObservedObject var currentlyReading: CurrentlyReading var body: some View { VStack { BookCard( currentlyReading: currentlyReading) HStack { Button(action: presentEditor) { /* … */ } .disabled(currentlyReading.isFinished) Toggle( isOn: $currentlyReading.isFinished ) { Label( "I'm Done", systemImage: "checkmark.circle.fill") } } //… } } }
-
19:58 - CoverImageLoader
class CoverImageLoader: ObservableObject { @Published public private(set) var image: Image? = nil func load(_ name: String) { // … } func cancel() { // … } deinit { cancel() } }
-
20:20 - BookCoverView
struct BookCoverView: View { @StateObject var loader = CoverImageLoader() var coverName: String var size: CGFloat var body: some View { CoverImage(loader.image, size: size) .onAppear { loader.load(coverName) } } }
-
25:36 - ReadingListViewer (Bad)
struct ReadingListViewer: View { var body: some View { NavigationView { ReadingList() Placeholder() } } } struct ReadingList: View { @ObservedObject var store = ReadingListStore() var body: some View { // ... } }
-
26:39 - ReadingListViewer (Good)
struct ReadingListViewer: View { var body: some View { NavigationView { ReadingList() Placeholder() } } } struct ReadingList: View { @StateObject var store = ReadingListStore() var body: some View { // ... } }
-
30:52 - App-wide Source of Truth
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } }
-
32:43 - SceneStorage
struct ReadingListViewer: View { @SceneStorage("selection") var selection: String? var body: some View { NavigationView { ReadingList(selection: $selection) BookDetailPlaceholder() } } }
-
33:49 - AppStorage
struct BookClubSettings: View { @AppStorage("updateArtwork") private var updateArtwork = true @AppStorage("syncProgress") private var syncProgress = true var body: some View { Form { Toggle(isOn: $updateArtwork) { //... } Toggle(isOn: $syncProgress) { //... } } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。