大多数浏览器和
Developer App 均支持流媒体播放。
-
Combine 实践
拓展您的 Combine 知识,这是 Apple 新推出的统一声明式框架,用于随时间处理值。立即了解如何正确处理错误、调度工作并将 Combine 整合到您的 app 中。
资源
相关视频
WWDC19
-
下载
大家好 大家好 我是 Michael LeHew 在 Apple Foundation 团队工作 今天 我很激动地 向大家介绍 今年即将发布的 Combine 框架 先澄清一点 我们要说的不是联合收割机
深入探讨之前 我想 先总体简单介绍一下 Combine 是什么 代码中 常见的 情况是 我们有 值或事件的发布者 还有想要从发布者处 接收值的订阅者 有一些相关方 会在这两者 之间建立连接
连接建立后 订阅者 会声明 想要从发布者处 接收值 之后 发布者便可以 向下游发送值 这一过程会持续下去 直到发布者因为值发送完毕 或出现发送失败 决定停止发送值 或者有人 选择取消订阅
如大家所见 这样的通信模式在我们的 软件中随处可见 形式可以是回调 闭包 或者其他出现异步通信的情况
Combine 正与这种模式 密切相关
Combine 定义了一个 统一的抽象 用来描述 API 这类 API 可以在一段时间内处理值 我们来看看 值发布者有哪些具体特征
我们已经在介绍会议中 谈论过这方面的内容 但我们再回顾一下 Combine 中的值发布者遵循
Publisher 协议 协议规定了两种 associatedtype 即发布者 发布的值 以及发布能否失败 稍后 我会详细介绍失败
Publisher 也要描述 如何将 Subscriber 附加于自身 限制条件是 associatedtype 必须匹配
就是这样 好了 我觉得理论部分足够了 此次会议名为 Combine in Practice 我们就来关注实践 我有一个巫师朋友 他非常酷 想跟我一起 为他正在筹办的新巫师学校开发一款 App App 中有一项功能是 提供法术下载 这些法术 是由和他一样的巫师分享的 他不是 App 开发者 他是个巫师 所以他给了我 一张草图 这就是我要使用的 UI 组件
虽然他是个巫师 但他确实会写代码 起码足够帮我下载法术 于是 他就开始忙自己的 我要做的则是 讨论我们要如何 使用 Combine 获取 必要的 App 值 在这个标签中填充法术名
Combine 中 NotificationCenter 支持将通知 提供给发布者 我们就来创建 一个 Publisher 让我的巫师朋友传递通知 这个函数的 返回类型将是 Publisher 但 Combine 中 对发布者重要的是 它的输出和失败类型
NotificationCenter Publisher 传递通知 并且不可以失败 既然我们要 详细探讨发布者 接下来的讨论中 我就会一直 把发布者的输出 放在上面 把失败放在下面 那么我们就有了一个 通知发布者 但我们真正想要的是 其中描述刚刚下载的法术的数据
我的朋友告诉我 他把数据放在了 userInfo 字典中 幸运的是 Combine 提供了 很有帮助的 map 函数 让我们可以深入内部 将通知转换为我们需要的形式 这与 Sequence 中 已有的操作很相似 可以看到 我们使用的发布者 输出是数据 不会产生错误 我们调用 map 这样的函数 作用于 Publisher 且返回 新 Publisher 操作符 它们在 Combine 中会经常那个出现 我的朋友还告诉我 JSON 负载 或者将要 成为 JSON 负载类型的数据 已经在我们的 App 中定义了 所以 我就可以用另一个 Combine 操作符 尝试解码数据 我们调用这个 tryMap 操作符
它和 map 很类似 但它多了一个功能 可以将错误抛出转换为流中的失败
确实 这个操作符的输出 是 MagicTrick 的发布者 它的失败 遵循 Swift Error 协议
从数据中解码自定义类型 是常见任务 我们也提供了一个操作符 专门帮你处理
仅需要简单的调用 .decode
发布者的输出 发布者的输出和 失败类型没有变化
既然我们有了可以 失败的发布者 我想多谈一谈我们 能做的事情 Combine 中 对潜在的失败 作出反应至关重要 每个发布者和订阅者 都有机会描述 它们生成或允许的 失败类型 我们将其内置于 Combine 因为就像 Swift 一样 我们不想让错误处理 成为一件完全 基于惯例的事情 我们在其他语言中进行了尝试 效果不太好 所以 许多类型会将其 失败类型描述为 never 这就表明 它们可能失败 或预期失败在上游得到解决 但除此之外 我们还提供了许多操作符 失败发生时 你可以借助它们作出反应或恢复 其中最简单的 就是 预设失败永远不会发生
自然 此时返回的发布者 失败类型 就是 never 但我们来看看为什么 想象一下 我们有一个上游的发布者 它与下游的 订阅者通过中间的 assertNoFailure 这一操作符连接 这个操作符只会 在收到值时直接将其向下传递
但假如上游 传来错误 我们的程序 就会卡住 这对于我们的巫师用户来说 不是理想中的结果
幸运的是 我们在 Combine 中 有许多其他操作符 来处理失败 除了预设之外 你还可以尝试 重新连接至上游 发布者或将错误 转换为其他类型 有一个操作符特别有用 那就是 catch catch 允许你提供一个闭包 其中定义一个恢复性 发布者 它适用于 原先的上游发布者发生失败的情形 我们来看看它如何生效 我们先来看看与之前 类似的情形 这次我们不用 assertNoFailure 而是 使用 catch 操作符 和先前一样 值会无障碍转发至 下游的订阅者 但错误抵达时 现有的上游连接将被终止
我们之后会调用提供的 恢复性闭包 它可以 产生一个新的发布者 供我们订阅 之后也可以 从它那里接收值
这样一来 catch 这个操作符 就可以用新的发布者 替换原有的发布者 让我们从错误中恢复 我们这就把它用在 我们的代码里
catch 的使用和 其他操作符很类似 但这里的闭包 希望我们返回一个发布者
Combine 定义了一种特殊 发布者供已有 想要发布的值时使用 我们称其为 Just 意为直接发布这个值 这是 Combine 初始 自带的诸多发布者 中的一个例子
使用它的时候 返回的发布者类型不能失败
现在 我来回顾一下我们 执行过的各种转换 最开始 我们有通知发布者 之后 我们对其映射 获取我们 想要解码的数据
之后 我们利用 .decode 操作符将数据 转换成用户定义的类型
但由于各种原因 解码可能失败 我们就要在失败发生时 将上游替换成占位符 来解决问题
但等一下 如果我们改用 恢复性发布者的话 我们就再也看不到 另一个通知了 我们终止了订阅 我们真正想要的 是能够尝试解码 并在失败时使用占位符 且同时与先前的上游 保持联系 自然 Combine 为此 也提供了一个操作符 它叫做 flatMap
flatMap 和 map 原理很像 名称也类似
你从上游发布者 获得值 但同时预期为获得的值 生成新的发布者
flatMap 便可处理 订阅这个嵌套 发布者的细节 并将值传递给下游
我们先来看看它的工作原理 之后再看代码
和以前一样 值从上游进入我们的 flatMap 操作符
值到达后 flatMap 会调用 一个闭包 将值转换为 一个新的发布者 此时 这个新的发布者是 一个 Just 一个 decode 以及一个 catch 和之前相似
flatMap 之后会订阅 这个新发布者将 结果值传到下游
我想在这个 flatMap 中 追溯另一个值 但这次 请想象 解码在运行中生成 一个错误 当失败抵达 catch 时 它会被 恢复性发布者取代 这个发布者 会被返回至 flatMap
这就可以确保 该操作永远不会失败
现在我们来看看 代码中该怎么用 我们从先前停下的地方继续 也就是我们处理 流中第一个错误的地方 但现在 我们来使用 flatMap 操作符 更改真的很容易
和用 catch 一样 我们用 Just 从收到的数据 创建新的发布者 这就是我们刚从 map 操作符中解码的数据 借助 flatMap 操作符的 嵌套域 我们就可以 依次 return .decode .catch 并将结果返回给 flatMap 此时 flatMap 会 订阅这个发布者 产生的发布者就会是 MagicTrick 的发布者 且不会失败 我们处理完 上游失败后 需要继续完成 既定目标 也就是尝试 发布法术的名字 有了 Combine 这非常简单 使用另一个操作符 publisher(for:) 操作符即可 我们用它来进入 MagicTrick 通过一个类型安全的关键路径 并生成一个新的发布者 本例中是 发布者返回的类型为字符串
现在 我想谈谈 最后一种操作符 它能提供一些 强大的功能 我们称其为定时操作符 就像在现实生活中 给东西定时一样 定时操作符可以描述 特定事件何时何地传递
这类操作符受到 RunLoop 和 DispatchQueues 的 原生支持 定时操作符的 例子包括
delay 操作符 可以将 事件传递延后至未来时间
还有 throttle 可以 确保事件传递 不超过某个 特定速率
其他操作符 包括 receive(on:) 可以确保 下游接收的事件 可按照特定的 线程或队列传递 我们就来使用这个操作符 确保法术的名字 始终在主队列中传递
可以看到输出 和失败的类型没有变化 这其实对定时操作符 来说很常见 我们再来研究一下 发布者链剩下的代码
我们之前说到 flatMap
这里我们用 publisher(for:) 深入 MagicTrick 内部 提取法术的名称
最后 我们把工作 转移到主线程上 用的是 receive(on:) 操作符 如果我们使用的是 AppKit 或 UIKit 其中 UI 依据主线程上下文更新 那现在就实现了 发布的值已经处在 正确的线程上了
如你所见 我们现在 已经能用发布者和 其操作符做很多事情了 我们在最初的 方案上 不断添加操作符 依次进行修改 最终循序渐进 产生了强类型值 我们看到发布者 能同步产生值 正如在 Just 的例子中 同时 它还能异步运作 例如使用 NotificationCenter 但现在 我想关注 值发布的 另一面 也就是值的接收
现在我想谈谈订阅者
和发布者一样 Combine 中的订阅者有两个 associatedtype 即它们的输入 和它们容许的失败
它们也描述了三个 事件函数 分别对应 接收订阅 接收值 以及结束 这三个函数调用的 顺序经过严格定义 总结起来 遵循 三条规则 规则一 回应 订阅调用时 发布者 只会调用一次 receive(subscription:) 不多不少
规则二 发布者之后 可以提供零个或更多值 传递给下游的订阅者 以回应订阅者的请求
规则三 发布者 至多只能发送一个结束信号 这个结束 可以表明发布者 已经完成 或者 发生了失败 这个结束信号一旦发出 便不会有其他值被传出 这三条规则可以 归纳如下 订阅者只会收到 一个订阅 之后是 零个或多个值 可能由一个结束终止 表明发布完成或失败
我之所以说 可能 是因为结束是可选的 许多特定流理论上 可以无限进行 例如先前的 NotificationCenter 示例
Combine 中 我们支持 各种各样的订阅者 我想向大家展示一下它们如何工作
我们回到之前的发布者示例 但我们现在 想要了解的是 我们正在使用的发布者 我们就先腾出点儿地方来
然后添加一个订阅者
这里 我添加了 Combine 中最简单的订阅类型 关键路径赋值 使用的是 assign(to: on:)这个操作符 它可以确保 上游发布者释放的 任何值都会赋值给 特定关键路径 下的特定对象 而就是从现在起 我们几乎可以随意 使用任何发布者并为 任何属性赋值 功能非常强大 这个操作符还会产生一个 取消令牌 你可以之后调用它 从而终止订阅 我想多谈谈取消 我们在 Combine 中 构建了取消 因为它便于在发布者 结束传递事件之前 终止订阅
如果你想释放 与此订阅相关的资源 这一点尤其有用
当然 取消是一种 尽力服务 但它可以满足你的需求 为订阅者取消订阅
我们引入了新协议 用以描述 可以取消或被取消的事物 我们引入了 一个极为便利的类 叫做 AnyCancellable 它带来的好处是 它可以自动 在清理阶段时调用 cancel
这可以显著减少 你需要专门调用 cancel 的次数 你只需要依靠 Swift 提供的强大 内存管理能力即可
我们接下来看看 第二种订阅形式 这里用到了 sink 操作符 这种方法很棒 你只要提供一个闭包 对收到的任何值 这个闭包都会被调用 你就可以对它进行 你想要的任何额外修改 和 assign 一样 sink 会返回 一个 canceller 之后你可以用它来终止订阅
第三种订阅方式是 两者的结合 我们称它们为主题 它们的行为既有点像 发布者也有点儿像订阅者
它们一般支持 将收到的值发送给多个目标 格外重要的是 你可以用它们 命令式的发送值 这一点极其重要 特别是当你在已有的 代码基础上工作的时候
我们来看看它们是 如何工作的 之后再展示 如何在实践中运用 我之前提到 使用主题 便可以向多个 下游订阅者广播 也可以 命令式发送值
任何收到的值都会 广播给所有下游订阅者
如果值都是由上游 发布者产生的 也会如此广播
Combine 中 我们支持两种主题 一种是 Passthrough 其中不存储值 只有你订阅这个主题的时候 你才能看到值
我们还支持 CurrentValue 主题 它保留了收到的 最后一个值的历史记录 这样新的订阅者 就能很好的交接
现在 来看看它们在实际操作中的样子 和之前一样 我们先来看发布者
创建主题非常简单 只需选取你需要的主题 指明输出和 失败类型并调用构造函数
主题的行为和订阅者类似 因为它们都要订阅 上游发布者 同时 它也类似发布者 需要调用我今天 提到过的操作符 比如 sink 等 来形成订阅自己的订阅者 你甚至还可以命令式的发送值 比如这个有魔力的词语
事实上 主题非常常用 我们甚至定义了 操作符 来向流中 注入主题 例如 Share 它可以将 Passthrough 主题 注入流中 主题的功能非常非常强大 你会发现很多 有趣的使用方法 接下来 我想 换个话题 谈谈 第四种 也是最后一种订阅者 就是与 SwiftUI 集成
SwiftUI 了不起的一点 就是你只需要 在 App 中 描述依赖 剩下的就可交给框架代劳
就 Combine 而言 这意味着你只需要 提供发布者 描述数据何时改变 如何改变
为此 你只需要 让自定义类型遵从 BindableObject 协议
SwiftUI 中的 BindableObjects 只有一个 associatedtype 这个发布者 限制为永远不失败 这可以与 UI 框架 配合得天衣无缝 因为这种语言的 类型系统强制要求你 先处理上游错误 再接触发布者
最后 你指定 一个名为 didChange 的属性 产生实际发布者来通知你 你的类型何时改变 就这样
要想进一步了解 SwiftUI 中 数据流的工作原理 我强烈建议 大家观看 Data Flow Through SwiftUI 会议 我们在其中 深入探讨了许多 可以实现的细节 但为了让大家体验一下 我来向大家展示它的实际表现
首先 我们从 巫师学校 App 中的 现有模型开始 之后 我们添加 遵循 BindableObject 现在 我们用 主题来描述我们的 模型对象如何改变
我们真的不需要 主题传递任何 特定种类的值 因为这个框架可以根据我们 对主体方法的调用 自行处理 我们就选 void 作为 主题输出的类型
像这样使用主题 十分灵活 因为现在 我们可以在对象变化时 命令式发送信息
但现在 我们就先使用一些 属性观察器 直接在主题中调用 send 当任一属性变化时 表明我们的 模型对象也变化了 接下来 我们要将这个模型 与 SwiftUI 视图挂钩 操作如下 我们声明一个 ObjectBinding 类型的模型 这样 SwiftUI 就能自动 发现并订阅我们的发布者 之后 我们在 body 属性引用 模型的属性 就这样
SwiftUI 会自动 在你表明模型发生变化时 生成新的主体
我想大家展示了 Combine 有非常多的内置 功能 大家可以 自己组织 合成许多 强大的功能
我们非常兴奋 因为有了新框架 你就能够大幅 简化异步数据流 要展示这一点 我的同事 Ben 会上台 告诉大家 如何 进一步将这些 强大功能整合进 你现有的 App 中 谢谢大家 谢谢 Michael 我很高兴今天在这里 见到大家
设计 Combine 时 我们高度重视构成 如大家在 Michael 的 示例中所见 我们从一个 简单的发布者开始 经过 各种转换后 创造了最终的发布者实现了目标 我们来看个例子 我们要在 App 中提供注册功能 这样 巫师就能在我们的 巫师学校注册了 我们有一些要求
首先 我们要保证 用户名通过服务器验证 第二 我们有密码字段 和密码验证 我们要保证二者相同 且长度都大于八个字符
最后 我们要保证 如果这些条件 都满足 我们就能启用或禁用 UI
所以 这个例子中 我们有 异步行为 有一些设备本地的 同步行为 我们 还要能够把它们 组合起来 我们来看看 Combine 能 提供什么帮助
首先 我要用 Interface Builder 为密码字段 在值变化属性上 创建目标动作
在代码中使用后 只要用户在这些字段中输入 我们就能收到信号 我们会获取当前值的 文本属性 并将其存入实例变量
但我们想把它们 和其他行为组合 特别是我们先前 提到的同步行为该怎么做
很简单 只要将 Published 添加至每个属性 我们就能 对他们添加发布者
Published 是一个属性包装器 它使用 Swift 5.1 中的新特性 将发布者添加到 任何给定属性
我们通过几个简单例子 来看看如何使用它
Published 属性包装器 按你的需求 添加在属性前
在代码中使用时 就和以前一样 我们也可以储存它 得到字符串值 这个例子中 currentPassword 现在是字符串 1234
它的特殊之处在于 我们要用 $ 作前缀引用它 这样 我们就在访问 被包装的值 我们之后就能使用 平常对发布者 使用的操作符或订阅它 此时要用 sink 之后 如果我们要 再将属性设定成另一个 伟大的密码 password 我们的订阅者就会在变化时收到值 显然 这个人没有 关注密码安全
我们谈到 要对 我们的两个发布者 同时进行判断
我们为它们添加了 Published 属性 并添加了 两个发布者也就是 被发布的字符串 不会失败 我们想要的结果是 只发布一个 验证过的密码
我们为此有一个操作符 叫做 CombineLatest
这里是我们之前 提到的两个属性 借助 CombineLatest 我们就能 引用带美元符号 前缀的属性包装器 之后 当任何一个改变时 我们就能收到信号 比方说 如果用户 已经在密码字段输入了 现在开始在 密码确认字段中输入内容 PasswordAgain 就会改变 而 Password 仍是 原先输入第一个字段的值 之后 我们就能用闭包 确保我们符合 行业要求 也就是 二者要相同 且长度 都大于八个字符 否则 我们就返回 nil 因为我们要将 这个信号与其他信号 共同使用 判断我们的 表单是否有效 为此 我们将 nil 作为我们的信号
所以 和类型一样 类型反映了我们采取的措施 我们可以直接在 代码中读出 我们有两个 Pulished 的字符串 我们组合了它们最新的值 最后获得了可选字符串
但如果我们的要求是 想要确保 人们不去用那些糟糕的密码 并添加一个 Map 呢
可以看到 类型在这里发生了变化 可以看出 我们组合了 两个 Published 字符串的 最新值 用 map 处理 得到可选字符串
这很棒 可以在 几乎所有用例中 帮助调试 但这个例子中 我们就 把它作为 API 边界 希望将它 与其他发布者组合 所以 如果我们 能只关注重点的话 岂不美哉 这里的重点是 这是一个 可选字符串发布者不会失败
为此 我们有一个操作符 叫做 eraseToAnyPublisher 可以返回可选字符串或 never 的 AnyPublisher 可以看到 类型 并没有变化 但 我们确实可以把它 当作我们想要的 API 边界推广 还能顺便 隐藏所有的实现细节
来回顾一下我们 都做了什么 我们的 初始属性是字符串 为它添加了字符串发布者 其中使用了 Published 属性包装器 我们之后使用了 CombineLatest 组合了这两个发布者的 的最新值 并添加了 我们的业务逻辑 之后我们使用 map 来过滤 糟糕的密码 最后 我们用了 eraseToAnyPublishser 因为这是 API 边界 我们要将它和别的东西组合起来
真了不起 我们有了第一个发布者
接下来 我们还想 为一些异步活动 构建模型 我们要保证用户名通过 服务器验证 这需要 快速处理用户输入 和之前一样 我们给 字符串属性存储添加 Pulished 属性 再给 valueChanged 属性挂上一个目标动作 但这里有点特殊 因为我们不想 用户每输入 一个字符 就 发起一次网络运行 不然 服务器会被挤爆的 我们要让信号 发送更顺畅 为此 我们有函数防抖 你可以用函数防抖 制定你想接收值的窗口 且接收值 不早于这个窗口 我们来看个实例
这是我们的上游发布者 这个例子中 这是一个 文本字段 中间 是函数防抖 如果用户打字很快
你就会看到 信号发送很快 但我们可以使信号发送平滑 在窗口中只传递一个信号
这很好 但我们还能 做得更好
如果用户在窗口中打字 而值最终 一直都是相同的 就没有理由再 呼叫服务器 看用户名是否有效 所以 如果用户输入 Merlin 我们会获取值 如果删除最后的 n 再输入 n 就又成了 Merlin 但我们也不用再次 呼叫服务器 removeDuplicates 操作符就是 实现这个的 它可以确保我们不用 在窗口内一遍 又一遍地发布同一个值 再来看代码 我们给 用户名属性添加了 Published 之后我们使用函数防抖 消除信号传输抖动
最后 我们移除了重复项 但我们还没有处理完 异步操作 我们刚刚平滑了信号 我们真正想要的是 查询服务器 看看 用户名是否有效 我们的 App 中 已经有了一个函数 叫做 usernameAvailable
接下来 我要将它作为 发布者引入
从 Michael 的例子中 我们 了解到 flatMap 能让你 从流中获取值 并返回新的发布者 我们该如何调用它呢
为此 我们有一个叫 Future 的东西 当你 构造的时候 你为它提供一个 闭包 其中包括一个 promise promise 就是一个 闭包 其中包含结果 可能是成功或失败
它的使用非常直观 我们调用 usernameAvailable 函数 当它异步完成 我们 获得值之后 我们在 promise 中填充结果 这个例子里是 success 和之前一样 我们表明 如果不可用 结果是 nil
我们来回顾一下步骤 最初 我们有一个很简单的发布者 也就是用户名发布者 我们采用函数防抖 平滑信号 并移除了 窗口中的全部重复项
之后 我们使用 Future 包装 现有 API 来构造 异步网络调用 我们使用了 flatMap 来 构造流分支
之后 我们将它添加至任一 发布者因为它是 API 边界 现在我们就创造了 两个自定义发布者 分别是 validatedPassword 和 validatedUsername 接下来我们要组合它们
现在 我们要做的是 利用两个信号 一个是 设备本地信号 另一个是异步 网络调用 并使用它们 启用或禁用 UI 我们已经知道该怎么做了
我们使用 CombineLatest 操作符 我们要获取之前 创造的两个发布者
我们要检查它们是否可用 这个例子中 我们只 返回一个元组 其中以可选 包含全部证书 如果没有 则返回 nil
将这些与你的 UI 连接起来其实非常简单
我们为登录按钮编写一个 输出口 我们创建一个实例变量来储存 这个订阅 这样我们 就能在这个视图控制器的 全生命周期保留它 因为我们想要 在展示表单的全过程中 启用或禁用这个按钮
所以我们就存储它 我们将它映射到一个布尔值 因为我们想要把它 赋值给按钮的 isEnabled 属性
最后 我们使用 receive(on:) 来切换到主线程 这是我们需要对 任何 UI 代码做的 之后 我们使用 assign 操作符 将它赋值给 给定关键路径的 signupButton 很了不起 我们有了一切需要的东西
总体来看 我们最初只有 三个非常简单的发布者 它们只能发布字符串
之后 我们使用组合 不断采取小改动 组建起了这些 创建了 最终的链条 之后 我们将它们组合起来 将它们赋值给按钮
这就是 Combine 的意义所在
所以 我建议大家马上上手 将你 App 的小部分 组合成自定义 发布者 找出 可以拆分成小型 发布者的小块逻辑 然后 不断使用组合 将它们 全部连接起来 你完全可以逐渐采用 你不需要马上 改变所有东西 可以有所选择 我们认为 你可以使用 Future 实现现在已有的功能 你可以使用 Future 组合回调和其他内容 如我们刚才所见
欲知更多信息 请观看 我们的介绍 Combine 会议 以及 SwiftUI 的数据流会议 今天晚些时候 我们 也会在 AppKit 实验室 谢谢大家 [掌声]
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。