大多数浏览器和
Developer App 均支持流媒体播放。
-
通过 SwiftUI 的数据流
SwiftUI 的设计初衷就是为了帮助您编写精美、正确且始终一致的用户界面。了解如何将您的数据做为依赖项进行连接,同时保持 UI 完全可预测且不含错误。熟悉 SwiftUI 的强大数据流工具,并认识每种场景下最合适的工具。
资源
相关视频
WWDC19
-
下载
上午好 欢迎 大家来到 SwiftUI 中的数据流 我叫 Luca Bernardi 稍后会有一位我的朋友 也是我的同事 Raj Ramamaurthy 加入讲演 大家对 SwiftUI 感到兴奋吗 兴奋 很好 今天来到这儿 我感到非常高兴 SwiftUI 是制作一个优秀 App 的 最便捷的途径 但是我们也从头开始设计 目标是降低 UI 开发的复杂程度 这就意味着数据在 SwiftUI 中 是第一类对象 在这次讲演中 我们将向大家展示一个 简单却有用的工具 你可以用它 使数据在视图层级中流动 这个工具可以帮助你 设计美观且好用的 App
我们也将会深入内部机制 看看 SwiftUI 如何更新视图层级 来确保 你可以正确而又持续地 呈现你的数据
最后我们将为你 在脑海中建立一个框架 去理解你的数据 和可用工具
在继续说明之前 我们必须要问 数据是什么意思
数据是所有 驱动你 UI 的信息 数据的形态结构多种多样 一个例子是 像 Toggle 中的状态一样 声明你的 UI
数据还代表模型数据 比如驱动一列信息的对象
我们有许多工具 供你选择 取决于你想做什么 你也许已经在之前的讲解中 看到了这些工具 如果你对它们并不熟悉 不必担心 我们会解释它们是什么 以及什么时候用得到 听完讲解后 你就会清楚地知道使用哪个工具 以及什么时候使用 但是在展示工具之前 我想先阐释 两个指导性原则 这两个原则启发了我们的设计 第一个原则是 每次你在视图中 读取数据时 你都在为那个视图创建一个依赖 这是一个依赖 因为每当数据发生变化 你的视图也要变化 去反映一个新的值
举个例子 这里蓝色的是播放控制的视图 这个视图需要 读取紫色的数据 每当这个值变化时 我们都要更新视图
所以 定义这个依赖 是个手动的过程 很快就会变得复杂起来 正如查看 SwiftUI 数据依赖 也是声明式的 并不是手动 同步或失效 通过 SwiftUI 你只需用很少的工具 把依赖描述给框架 框架就会处理剩下的 这就意味着你可以 将你的注意力集中在 为用户提供最佳体验上
第二条原则是 在视图层级中 你读取的每一条数据 都有一个数据源
数据源存在 在视图层级里 比如说你有状态 要折叠 或者不折叠 它也可以是外部的 比如当你显示一个 来自不变模型的信息时
不论数据源在哪里 你应该一直有一个 单一数据源
重复数据源会 会产生 Bug 和不一致 你要一直很小心 让它们保持同步
想一想你一直重复 同一条数据 比如在同级视图里重复同一条数据 想一想消息传递 是多么复杂 KV 观察 或响应一个不同的 事件顺序 会产生 Bug
很容易就出错了 它们会重复同一个错误 很多次 反而 你需要做的是 把数据提取到一个 共同的父级节点 让两个子节点链接到父节点
当你有一个单一数据源时 你就消除了这种 视图和数据 不一致的 Bug 你就可以用 语言有的工具 在你的数据中执行变量 记住这个原则 退一步 看看 你代码里的所有数据源 用这个原则 去决定 数据的结构 在我来 Apple 总部 上班的路上 我特别喜欢听一个很棒的播客 我觉得用 SwiftUI 做一个播放器是个好主意 在这次讲解中 我会用这个做例子 展示所有的 可用工具
这是我们将要建构的 UI
这是播放器的界面 我们需要它显示 节目和剧集的名字 播放按钮 和当前时间 我们将一步一步 建构这个 UI 首先创建一个视图 显示当前剧集的视图
第一步是创建一个新视图 PlayerView 这个视图有一个属性 存储着当前播放剧集 我们还想要显示 剧集和标题 所以在主体中 我们会做一个 VStack 包含两个文本 这样它们会纵向排列 基本的 Swift 属性 是你的第一个工具 当你有一个视图 需要读取所有的派生数据访问时 它是很好用的 这条数据会被它的父节点 提供给视图
现在我想让它有 播放和暂停的功能 我们有一个新的属性 表明当前剧集是否在播放 现在 显示不同片段的图像 取决于 isPlaying 这个值 但是现在我想让播放键 有交互功能 用户按播放键时 播放状态会切换 图片也会随之改变 我们可以用 Button (按钮) Button 包含一些内容 运行一个动作 当用户点击这个它时
我们只是切换 isPlaying 但如果我们运行一下 就会出现编译错误
这是个好事情 通过 UI 的 其中一条普遍原则 把我们引上正确的道路 我们不改写视图层级 你的 UI 每次更新 都是因为某个视图主体 在生成新的值 为了处理这种情况 我们有一个工具 叫作 State (状态)
我们在这个视图里创建一个主状态 通过 isPlaying 属性上的 State Property Wrapper 我们可以实现这一点 这么做相当于 告诉系统 isPlaying 是个值 它可以变化 播放器视图会随它变化
现在再运行一下 就不会出现编辑错误了 用户每轻点一次按钮 状态值都会变化 框架就会为这个视图 生成一个新主体 如果你不了解 Property Wrapper 它是 Swift 5.1 里非常有用的一点 至于它们是如何运作的 我们不再细说 如果你想了解更多的话 可以去看这两个很好的分会议 在今天这个讲演中 你只需要知道 当你添加 Property Wrapper 时 你就是在打包这个属性 并当它读写时 为它增加一些额外操作 你可能想知道 这是怎么实现的 这个额外操作 是什么 当你声明框架 为视图上的变量 分配永久存储时 我们会追踪它为一个依赖 因为如果系统给你 创建了一个存储 你必须总是明确规定 一个初始常量值 视图经常会被系统重建 但是对于 State 来说 尽管视图同样有很多次更新 框架知道它需要 永久存储 这是个很好的操作 清楚地声明状态属性 是私有变量 真正实现 状态由视图所有和管理 但是我想深入内部结构 让你们看看 当用户点击按钮时 会发生什么
从我们刚才展示的 视图层级开始
我们刚才说 当我们定义了一个状态时 框架会为你 分配永久存储
状态变量的一个特殊属性是 当它们变化时 SwiftUI 就开始了 因为 SwiftUI 知道 状态变量正在写主体 它知道视图渲染 取决于状态变量
当用户和按钮互动时 框架就会运行动作 这个动作继而 会和某个状态关联
运行时数据 状态确实会变化 并验证状态的视图 这也就是说 它会重新计算那个视图的主体 以及它所有的子类 在这种意义上 所有的变化都会一直 贯穿你的视图层级 这样效率很高 这是因为 框架会比较视图 而且只会重新渲染改变了的地方 这正是我们 之前提到的 框架可以 为你管理依赖
我们刚才还提到数据源 要记住 每次你 声明一个状态 你就定义了一个数据源 这个数据源是属于视图的 这点很重要 我用了更大的字母来强调 另一个重要的点是 视图是状态的 一个函数 而非事件顺序 传统方法是 通过直接修改视图层级 响应某个事件 比如 通过添加或者删除 一个 subview 或改变 alpha
但是在 SwiftUI 里 你修改某个状态 这个状态就是数据源的函数 通过这个就可以生成视图 这就是 SwiftUI 声明式语法出色的地方 你鉴于当前状态 描述视图 SwiftUI 就是这样 降低了 UI 开发的复杂程度 让你可以写一个 好看又准确的交互界面
你可以把 App 看作 一个用户和设备之间的 持续反馈循环
一切都从用户开始
用户和 App 交互 生成一个动作 动作被框架执行 修改某个状态
系统发现状态发生了变化 所以它就知道 它需要根据状态 刷新视图
这个刷新给了 UI 一个新的形式 用户继续和新形式互动
在这个模型中 数据流向是单一向的 它是所有变化的 单一终点 让视图刷新变得 可预测且易理解 现在我们弄明白了状态 我想回到 AppWare 构建 并做一些优化 我想做的第一件事是 每次用户按下 暂停键 剧集名 都会变成灰色 我们已经知道怎么做了 我们只需要用 isPlaying 状态 选择正确的文字颜色 接下来我想做一些重构 如果你已经看过 SwiftUI 的 基本工具讲演 你就已经知道 视图在 SwiftUI 里 是一个 Locust 阻塞
不必害怕 将视图中有意义的数据 合并为可以被一起编写的 更小的 可重复使用的组件 正好可以以此为例 这是播放按钮和暂停按钮 的代码 封装这个逻辑 到它自己的视图 命名为 PlayButton
现在看一下 PlayButton 的实现 代码还是一样的 只是封装到了一个新视图 但是注意这里我们做了一个新状态 但是状态不是正确的工具
使用状态 我们就为 isPlaying 建立了一个新数据源 这样我们必须 和父级 PlayerView 的状态 保持同步 这不是我们想要的效果 我们想做的是 让它成为可重复使用的组件 所以这个视图不会拥有 一个数据源 它只能读取一个值 然后改写它
但是它不需要拥有状态 我们有处理这种情况的工具 这个工具叫作 Binding(绑定) 通过 Binding Property Wrapper 给数据源 定义一个清楚的依赖 而不拥有它
而且 你不需要 提供初始值 因为绑定可以从状态中生成
看一下效果如何 是不是很适合我们的例子 我们唯一要做的就是 用 Binding Property Wrapper 删除初始值 就是这么简单 现在来看一下 如何通过返回 PlayerView 为 PlayButton 提供一个绑定
PlayerView 依旧持有状态 这是你的数据源 在属性名字上 使用美元符号 你就可以从状态生成一个绑定 这是你让组件 通过绑定访问状态的方法 美元符号是 Property Wrapper 的 另一个特色 如果你想了解更多 请看现代 Swift API 设计讲演
我想我们可以暂停一秒 欣赏一下这是多么 简单却有效
PlayButton 不包含 isPlaying 值的主体
只是通过绑定 作为它的引用 所以没有必要 让数据和视图保持同步
我想把这个 和我们现在使用的 UIKit GraphKit 对比一下
我们有一个 View Controller 有多个视图 需要响应我们用户的交互 要非常麻烦地设定目标 动作 或者定义委托
你需要观察模型变化 并且也要响应事件
每次有值变化时 你都需要读取值 把它设置在任何需要它的地方 一旦你的 App 复杂起来 这就成了大问题 我确信 在座诸位都知道 我说的这种情况 View Controller 的整体目标 就是让你的数据 和你的视图保持同步 这是你我要处理的复杂难题 但是在 SwiftUI 中不存在 你有一个简单的工具 来定义数据依赖 框架会处理其他的 你再也不需要 View Controller 了
这个想法非常有用 这应用在整个框架中
如果你看一下组件的 API 如 Toggle TextField 和 Slider 它们都需要一个绑定框架 让你控制 数据源在的地方 你创建数据 把它给组件 只是作为一个引用 而不需要重复信息 或者费力保持同步 这真是太棒了
在 SwiftUI 视图中 有很多 App 可以查看 布局 导航等 实际上它们是你的 单一原生组件 它们也是好工具 用来为单一数据 封装表示逻辑
框架使你能够 并鼓励你创建小的视图 去呈现一条 可以被一起编写的
单个数据 再一次 框架带领你 编写小单元 让你更清楚明白 现在回到我们的例子
我把这个 UI 给我的设计师看了 她非常惊讶 没想到这么少的代码 就能有这些效果
她也提出了一些优化的建议
我们应该把播放和暂停的 过渡变化动画化
幸运的是 这很简单 因为框架追踪了 所有变化了的东西 难以置信 用状态驱动动画 是多么简单强大
用一个动画模块 通过打包改写给绑定 当值改变时 框架会动画这个过渡变化 直到最后的状态 你都会一直得到正确的动画
如果你想了解更多 关于 SwiftUI 强大的动画和布局系统 以及如何制作优秀 App 的信息 我建议大家看一下 在 SwiftUI 中构建自定义视图这个分会 现在我们已经了解了 State 和 Binding 但是 SwiftUI 还有一些绝活 大家没有看到 为了让大家了解得更多 我现在要请 Raj 上台 Raj
谢谢你 Luca 接下来我会介绍 一些其他工具 在 SwiftUI 中它们可以用来管理数据
听了这个讲演 你就可以设计和构建 稳固的可重复使用的组件 可以应用在各种数据上 大家刚才也看到了 我们有很多有用的工具 可以在 SwiftUI 中处理数据 Luca 已经介绍了一些 比如使用 State Binding 甚至只用 Swift Property 我会介绍 剩下的工具 以 SwiftUI 中的外部变化 作为开始 我们现在回到 Luca 刚才为大家展示的图解
在这个图解中 用户和 App 互动 这形成了一个动作 结果是改写了状态 这就生成了 视图的新主体 提供给用户
一些事件从外部被初始化 比如计时器和通知
但是记住 在 SwiftUI 中 你的视图是状态的函数
所有的改变 只有一个单一漏斗点
这意味着 SwiftUI 用响应用户操作的方法 响应外部更改
所以当计时器启动 或者接收到通知时 过程看起来是差不多的 我们创建了一个动作 执行一些状态改写 生成视图的新副本 重新提供给用户 在 SwiftUI 中 为表述这些外部事件 我们有单一抽象化
它被成为 Publisher Publisher 来自一个新的 组合框架 Combine 是一个统一声明式 API 随时间处理数据
今天我们 不会细说 Combine 但是大家一定要去看 一些相关的讲解 这样可以了解更多
要达到我们的目的 需记住一点 在 SwiftUI 里用 Publisher 时 它们应该删除主线程 Combine 提供了一种用操作符的简单方法 被称为 Receive On 想知道更多的话 可以去看 Combine and Practice 分会 现在我们通过例子来看一下 它是如何运行的
有时候用户 不知道自己听播客听到了哪儿 他们厌倦听年轻的一代 接连好几个小时 大谈特谈牛油果吐司
所以我们要为播客播放器 加一个时间戳 这样用户就知道 自己听到了哪儿
要实现这个效果 我们要加 State 表现当前时间 和描绘该值的文本 接下来我们会使用 onReceive 修饰符 很方便 我已经构建了一个 publisher 当前时间一改变 它就会启动 我会使用那个 publisher 传递给 onReceive 修饰符 另外 我还会给一个闭包 当 publisher 删除时 这个闭包会运行
就是这样 这么做 我们已经给 SwiftUI 描述了依赖
现在当 currentTime 刷新时 我们会刷新状态 SwiftUI 就会知道 那儿有一个依赖 标签就会自动刷新 就无需费力 去做失效或管理了
我们已经简单说明了 SwiftUI 里的外部变化
接下来我会介绍 外部数据
对于外部数据 我们有 BindableObject 协议
BindableObject 是 使用封装了的 测试过的 你已有的“true”模型的 简便方法
这对教 SwiftUI 你已经架构的引用类型模型 是非常有用的
这是你拥有并需要管理的数据 SwiftUI 只需要知道 如何在这个数据中响应改变 我们换个例子
用户希望 播客可以在他们 所有的设备上同步 我负责添加这个功能 所以我已经开始了 我已经建立了一个模型 现在是时候使用 我在视图层级里做好的模型 把它带到 我们的播客播放器 看看多么简单 这里是我们搭建的 模型的一个草图 要通过 SwiftUI 用这个模型 我要做的只是 确认它到 BindableObject 协议
用 BindableObject 我们需要提供的只是一个 publisher 这个 publisher 显示 对数据的更改 记住组合 publisher 是我们为表现 对 SwiftUI 外部更改的 单一抽象化 这里 我们会在 didChange property 提供一个 publisher PassthroughSubject 是一个 publisher 接着 SwiftUI 会订阅这个 publisher 所以它知道什么时候 更新我们的视图层级 在高级操作中
当我们改写模型时 我们只是简单地发送 publisher 请求
现在 注意 为保证正确 不管什么时候 模型一变化 我们就需要这样做 这样视图层级才能保持刷新 幸好 SwiftUI 可以帮助我们 它优雅地应对这些数据 帮我们很好且正确地 实现这个效果
现在我们已经搭建了模型 以及它对 BindableObject 协议的确认
接下来我将向大家展示 如何在视图层级中用模型
还记得之前说的两大原则吗 每一条数据都有一个数据源 当你访问那个数据时 你在上面创建了一个依赖 我们已经创建了数据源 但是我们还没有一个依赖 幸运的是 在你的可绑定对象上创建依赖 是非常简单的 这里有一个非常基础的图解 可以看到我们的视图层级在右侧 是蓝色的 我们的模型在左侧 是绿色的
现在我们连接二者 用 ObjectBinding Property Wrapper 创建一个依赖
我们这么做时 每一个有那个 Property Wrapper 的视图 都取决于我们之前写的模型
就像用 State 一样 当你用 ObjectBinding Property Wrapper 将它添加到视图中时 框架会识别出 那儿有一个依赖 所以在主体中 当你访问那个数据时 我们自动明白什么时候刷新视图 在代码里看起来就像这样 你创建视图时 添加 ObjectBinding Property Wrapper 到你视图里的一个 Property 当你实例化视图时 你只是将引用传递给 你已有的模型 注意 这在视图的实例中 创建了一个 清楚的依赖 这样很棒 因为每次我要实例化视图时 我都知道模型上 有个依赖
就是这样
我们这么做时 每个有 Property Wrapper 的视图 都会自动订阅 BindableObject 的变化 也就意味着我们实现了 依赖自动追踪 又不需要 失效和同步了
这里我想停一下 有一点需要强调 因为如果用 SwiftUI 是值类型的话 每次你使用引用类型时 你都应该用 ObjectBinding Property Wrapper 这样当数据改变时 框架就会知道 然后让你的视图层级随之改变 这就是如何在 BindableObject 上 如何使用 ObjectBinding 创建一个依赖 其实我们还有一个工具 也可以创建 这些依赖
我们可以创建间接依赖
所以我带了一个 和刚才大家看到的图解 很相似的一个图解 但是这次我们的视图中多了些子类 接下来 我想要引入 Environment
如果你看了 SwiftUI Essentials 讲解 你就知道 Environment 是非常好的封装 它可以推动数据 一路向下流过视图层级
使用 Environment Object Modifier 我们可以真正地 把 BindableObject 写入 Environment 现在 我们的模型在 Environment 里 我们可以用 EnvironmentObject Property Wrapper 在模型上
创建依赖 现在 通过使用这个 Property Wrapper 我们可以在那个模型上创建依赖
但是不止这样 你可以在很多地方 用到它 所以你可以 在整个层级的 各种视图中使用它 它们都依赖于同一个模型 当然 数据变化 一切都会自动 随之更新
你获得了 和 ObjectBinding 相同的 依赖追踪 你用这些工具 把依赖描述给 SwiftUI 框架会处理其他的部分 这很棒 这个便捷的方法 就可以更新我们的播客播放器
就像这样 你只需要添加 EnvironmentObject Property Wrapper 到视图 然后在视图 上方的父类中 只用 EnvironmentObject Modifier 提供模型
现在 不论什么时候 只要我们在主体中使用播放器 SwiftUI 就会自动替我们更新数据 你可能会想 什么时候用 EnvironmentObject 什么时候用 ObjectBinding 实际上你可以使用 ObjectBinding 构建整个 App 但是从出栈到进栈 传递模型 会比较冗长
这时候就需要 EnvironmentObject 了
这真的很方便 不直接在层级间 传输数据
这里 你可以看到 通过 EnvironmentObject 我们可以间接传送模型 通过视图层级 也就是说我们不需要 用模型实例化所有的即时视图 到视图层级
所以 Environment 真的是一个很好的方式 用来间接传送各种数据 一路通过 你的视图层级 你可能已经看过 它在重点色 或布局方向等方面的应用
正如 Luca 刚才所说 数据的形态结构多种多样
值 像是重点色 布局方向 它们只是数据
当你在视图中使用它们时 你就是在它们上面 创建依赖
实际上 Environment 是一个 通用集装箱 可以用来处理各种 间接数据和依赖 框架自由使用它 为你带来各种特色功能 比如动态类型 和深色模式 你还可以 在预览中使用 Environment 给重点色或主题等 赋新值
我们已经快速过了一遍 SwiftUI 里 处理数据的强大工具
现在我让大家 对如何使用正确的工具 和如何调配它们 有一个概念 这里主题之一是 每一条数据 都有单一数据源 在 SwiftUI 中 我们有两种选择 去处理这些数据源 第一种是 State
State 适合本地视图的数据 一个值类型
由框架 处理 分配 和创建 BindableObject 适合 你控制的数据
它适用于给 SwiftUI 展现 外部数据 比如在 onDevice 数据库 这里有你处理的内存 对你已有的模型 很有帮助 现在我们已经了解了 数据源 接下来 我想介绍一下 构建可重复使用的组件 SwiftUI 的一个优点是 视图是 低成本阻塞 这意味着 我们不需要 在架构和性能之间做折中 你可以搭建 你想搭建的架构 同时也能获得很好的性能 你不需要 权衡折中
通过 SwiftUI 你可以集中精力于 让你的视图成为 可重复使用的组件 当你这么做时 你可能会注意到 在视图中使用数据时 你也许不需要 改写它 所以当你可以不管它时 只读路径是更好的选择
在这一点上 我们有 Swift Property 和 Environment 因为视图在 SwiftUI 中是 值类型 框架可以自动确定 数据什么时候改变 视图也会随之变化 总的来说 你应该倾向于选择 不可改写访问 但是有时你确实需要改写值
这时我们有 Binding 正如 Luca 刚才告诉大家的 Binding 是数据的第一类对象 它可以让你的组件 在不拥有的情况下 读写数据 这对于可重复使用来说很有利 实际上 你可以 绑定到很多不同的 数据表现 今天我们将展示如何 绑定到 State 和如何绑定到 ObjectBinding 实际上 你也可以 绑定另一个绑定
你只需要用 我们之前展示的 美元符号前缀 它可以让你 从其他工具之一里 生成一个绑定 我想停一下让大家欣赏 这是多么有用 刚才 Luca 展示了 我们在 SwiftUI 里提供的组件 它们运行在 Binding 上
接下来以 Toggle 为例
Toggle 把绑定给到 Boolean
但是 SwiftUI 中数据的美观性在于 Toggle 不需要知道 或不需要在意 Boolean 在哪儿或来自哪儿 它要做的只是 知道如何读取和改变值
Binding 是一个工具 它可以封装这些操作 而且同时 不需要考虑 Toggle 这是在 SwiftUI 中使用数据的 真正魅力所在 你可以做到非常准确 而且不需要担心其他的
你会发现 在我讲解 搭建可重复使用组件的时候 我实际并没怎么提及 State State 被卡在你的 视图以及它的子类里 所以如果你的组件需要 操作一个值 一个来自外部或其他地方的值 State 也许不合适 对于原型开发第一步来说 State 是一个很好的工具 正如你今天看到的 它在我们播客播放器上的表现一样
但是大部分情况下 你的数据是存在于 SwiftUI 之外的
比如数据可能 在一个数据库里 这可能就需要其他东西来展现它 比如 BindableObject 所以如果你要用 State 的话 请退一步 考虑一下 数据真的需要 被视图拥有吗
也许数据 状态 需要被提到父类 就像刚才 Loca 展示的那样 或者数据可以 被外部源 通过 BindableObject 展现 所以使用数据时 需要非常小心 但它确实有它的优点
State 的一大用途是 我们框架里有按钮
按钮用 State 追踪 用户是否按下了按钮 然后以合适的方式高亮它
通过 State 处理按钮的好处是 当你创建了一个按钮 你不需要再考虑 高亮状态
数据真正 归按钮所有 所以当你要用 State 时 你需要考虑的是 情况是否和按钮一样 如果一样的话 State 或许是个很好的工具 如果情况不一样 那你就要考虑使用其他工具了 我们刚才也为大家展示了 这些 SwiftUI 中的工具
这就是如何用 SwiftUI 构建可重复使用组件 我们在这里向大家展示的是 对所有类型的软件都适用的 普遍情况
每个软件都有数据 而且每个软件都有 数据访问
仔细了解你的数据 尽量减少 数据源 构建可重复使用组件 你可以消除一整级的 Bug 使用 SwiftUI 时 应用这些观念 会非常简单 因为我们已经把它们 应用在框架里了
我们还有很多关于 SwiftUI 的讲解 我建议大家去看一下所有的相关介绍 这将会改变你搭建 App 的方式 谢谢大家 [掌声]
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。