大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 SwiftUI 中的并发
探索如何使用 Swift 的并发功能来构建更棒的 SwiftUI app。我们将展示并发工作流程如何与 ObservableObject 交互,并探索如何直接在 SwiftUI 视图和模型中使用。了解如何使用 await 使您的 app 在 SwiftUI 运行循环中顺利运行,并了解如何使用 AsyncImage API 快速获取远程图像。我们将带您了解在自定义视图中启用其他异步流的过程。
资源
相关视频
WWDC22
WWDC21
- 利用 Swift Actor 保护可变状态
- 探索 Swift 中的结构化并发
- 揭开 SwiftUI 的神秘面纱
- 认识 Swift 中的 async/await
- 认识面向 Swift 的 MusicKit
- SwiftUI 中的新功能
WWDC20
-
下载
嗨 欢迎参与 《探索SwiftUI中的并行性》 我是科特克里夫顿 SwiftUI团队工程师 稍后我的同事洁西卡会加入 Swift 5.5加入了不同的新工具 来管理Swift代码中的并行性 在本座谈中 洁西卡和我 会帮助你了解 这些东西会如何改善 你与SwiftUI应用之间的互动 我将会解释这些新工具 如何让你把数据模型做得更好 并向你展示SwiftUI 如何与新的main actor配合 然后洁西卡将向你展示如何将 并行数据模型 与SwiftUI视图连结 并介绍一些能发挥Swift 全新并行工具优势的全新API 为了充分理解 洁西卡和我将要分享的信息 对于Swift新并行的支持 具备一些背景知识非常重要 我们推荐你观看 《认识Swift中的async/await》 和《探索Swift中的架构化并行性》 再深入探讨此视频的其余部分 在我小时候 我一直梦想当宇航员 我有时在宇宙飞船上工作 但除此之外 那个童年的梦想 并没有实现 然而 我并没有失去对太空的热情 我决定应用我的实际技能 作为一名SwiftUI工程师 打造一个能 下载太空相关照片的应用 来看看我设计的应用 这个应用显示了几张随机的太空照片 这些颜色太美了 当看到一张喜欢的照片时 可以储存以便以后查看 为了取得这些美丽的影像 我的应用会通过REST API 与Web服务进行互动 这听起来像是 全新Swift并行功能的 完美使用方法 让我们从数据模型开始 我使用SpacePhoto结构 来储存单个影像的信息 结构具有标题 照片描述 图像发布日期 指向实际图像的URL等字段 我把我的类型设为Codable 我就能轻松实例化 来自服务器响应的实例 或将它们保存到盘上 而且可识别 让我能在ForEach 和其他数据驱动视图中使用它们 接下来 我想显示这些项目 为此我需要一个模型 来获取和持有他们的集合 在这里我使用了Photos类型 通过让Photos类型 遵循 ObservableObject 我的SwiftUI视图会自动更新 只要我的数据也有更新的话 我使用了已发布的属性 来储存一系列太空照片
为了从REST端点取得更新的item 你可以使用Update Items方法 我很快就会细谈这个问题 但首先 我想粗略地讲述一下 基本用户界面
这是我想构建的用户界面 到目前为止 我只有标签视图 和基本的照片视图
我的PhotoView拍摄一张太空照片 并显示其标题 这已经足够让我看到 我的数据模型在动作了 再来看看Catalog视图 我的Catalog视图会显示照片清单 为了做到这点 我会添加一个 StateObject 并用我的照片Observable Object来实例化 在我看来 我将添加导航视图 使用这里的导航视图将 让我新增一个大的导航标题 接下来 在导航视图中 我会添加一个清单 而在我的导航视图中 我会使用ForEach来映射我的照片 对它们显示照片视图
有了它 我可以看到我的样本数据
目前一切都如预期进行 但让我们再加以打磨一下
首先 这是我预期的导航标题 现在 预设的插图清单风格 在这里看起来很棒 但要真正展现我的太空照片 我想换成平淡的风格 好让照片能真正 从黑色背景中跳脱出来
我把listStyle设为plain 我使用了类似列举的静态成员语法 有了这个语法 SwiftUI 的风格修饰符 能获得更简洁的拼写 并更好地支持 Xcode 13中的自动完成功能 最后 让我使用SwiftUI 今年的另一个新功能: 列表分离器控制
在我的ForEach中 可以用listRowSeparator修饰符 来隐藏分离器
有时当我用SwiftUI 在打磨用户界面时 我发现很难停止 但现在暂时先不谈用户界面 在我讲完数据模型后 Jessica会接手继续讲 不过 在深入探讨数据模型之前 我只想谈谈 SwiftUI如何与 可观察对象进行交互 以及Swift 5.5中的 新并行功能 如何使交互相较过去 都更不容易出错 在WW 2020 《SwiftUI中的数据要点》里 我的同事拉什谈到了 SwiftUI的更新生命周期 我会将驱动此生命周期的代码 称为“执行循环” 在Swift 5.5中 执行循环是在Main Actor上运行 有关Actor的更多细节 请查看以下讲座 《Protect mutable state with Swift actors》 洁西卡和我会聚焦在本次的讲座 SwiftUI执行循环会接收 来自用户的事件 并允许你更新模型 然后将SwiftUI视图呈现到屏幕上 我喜欢称这些更新为 “执行循环的打勾” 让我们展开这个循环 方便查看连续多个打勾
在SwiftUI 中 ObservableObjects 可以以一些有趣的方式 与SwiftUI执行循环互动 让我们回到 Photos ObservableObject 看看updateItems方法 我要从我的SwiftUI视图呼叫 updateItems 它将在Main Actor上运行 使用这个蓝色矩形来显示 updateItems的运行时间 我想聚焦在这行代码上 我将获取的照片 指派到“items”属性 由于“items”是已发布的属性 这个任务会触发 objectWillChange事件 然后立即将撷取的照片 写入“item”的储存中 当SwiftUI看见了 objectWillChange时 会帮我的items拍摄快照 在快照后执行循环的下一个打勾中 SwiftUI会将快照与 目前的值进行比较 由于这些值不同 SwiftUI知道要依照Photos 来更新我的视图 请注意 由于objectWillChange 更新储存、执行循环打勾 都会发生在主要演员上 因此它们肯定会按顺序发生 在2020年的《数据要点》讲座中 Raj描述了 当视图在主体中 执行太多工作 则更新速度会变慢
如果你的模型代码对Main Actor 执行了太多的工作 则更新也会变得缓慢
例如 假设我的fetchPhotos函式 在等待下载完成时发生阻碍 并假设我的连线又很缓慢 因为我阻碍了主要演员 我错过了这个执行循环的打勾 这会变成用户可见的一个小故障 在过去 你可能已指派到 另一个队列去执行该工作 如此一来 昂贵的fetchPhotos 就脱离了主线程 这看样子还可以 但有个棘手的问题 我正在改变ObservableObject 脱离主要的演员 我所做的改变和运行循环的打勾 可能会交错 例如 当我分配给“items” 以及SwiftUI拍摄 objectWillChange的快照时 这种情况可能会 发生在运行循环的打勾之前 状态的更改尚未发生 因此SwiftUI会将快照与 不变值进行比较 实际的状态改变会在 执行循环打勾之后发生 因此我的视图没有更新 为了能够正确更新 SwiftUI需要让这些事件 按顺序发生: objectWillChange objectWillChange的状态 已经更新了 然后执行循环到达了它的下一个打勾 如果我能确保这些都会 在Main Actor上发生 我就能保证这个顺序 在Swift 5.5之前 我可能已经发回主队列 来更新我的状态 但现在要容易得多 只要使用await就好! 通过使用await 从Main Actor 发出异步呼叫 我能让Main Actor上的 其他工作继续 同时进行异步的工作 这叫做“让出”你的Main Actor
在UpdateItems中 我可以 使用await让出Main Actor 并传回给SwiftUI 在长期执行I/O期间 使它可以保持执行循环滴答作响 并避免任何UI故障 当同步工作完成时 Swift会将updateItems方法 重新输入到Main Actor上 以便我可以更新状态 来看看它是如何工作的 比起指派给另一个队列 我只要等待(await) 长期执行的操作结果 当我写await时 updateItems函式 会让出对Main Actor的控制 以便执行循环可以继续 当await完成抓取后 主要演员会重新输入我的函式 我就可以安全地 更新我已发布的属性 触发objectWillChange 并将新值提供给SwiftUI 让我们直接在Xcode上 看看我能不能做到获取 我在投影片上 显示的是updateItems方法 为了运行fetchPhotos 首先 先在抓取单张照片上添加代码 我会让我的fetchPhoto方法 从rest端点上获取照片的URL 并传回太空照片
接下来 我将在URLSession上 使用异步新版本的数据便捷 从URL中获取数据
为了对它进行stub 我使用了强制尝试 我很快就会把它整理干净
啊 数据方法是异步的 所以我需要使用await
这意味着我需要 让我的fetchPhoto方法异步
好的 很好 现在我有数据了 我将使用可解码的初始化器 来实例化一张照片 并交还它
接着来看看fetchPhotos 我已经把一些代码stub进去了 好获得随机选择的日期 和循环在他们身上 我想建立一个数组 所以我会把“下载”作为变量 并新增一个日期变量到我的循环
在循环中 我将呼叫一个 既有的辅助方法 来建构其余的端点URL 来获取特定的日期
再来我将呼叫fetchPhoto方法 并将结果附加到我的数组中
来建立吧 啊 因为fetchPhoto是异步的 我得等待结果
这意味着fetchPhoto 也需要异步
我要对fetchPhoto 依序做出这些呼叫 为了简单起见 请看Swift 5.5的工作群组 了解更强大的选项 现在 我只需要等待 fetchPhoto 就像幻灯片中显示的一样
有了它 我的更新逻辑就到位了 现在 也许你和我一样紧张 为了让这些被迫尝试能够顺利运行 让我们整理一下思路 现在 当下载失败时 我就会返回空值
然后 在fetchPhotos里 我只会将非空零值加入到我的数组中
现在那些照片使用了异步等待 我能确保它不会遇到任何 我先前说过的
棘手的objectWillChange臭虫 只要它是在主要演员上运行 但是 我怎样才能确保这一点呢? 幸运的是 Swift编译程序 可以帮助我 通过在照片中新增 @主要演员批注 编译程序会保证 照片上的属性和方法 只会从主要演员里存取 完成之后 模型就到位了 接下来 洁西卡会将我们的视图 与模型连结 并向你展示一些 很棒的新SwiftUI API 将并行性运用在你的应用程序当中 洁西卡?
谢谢你 科特 让我们切换到目录视图 并使用科特刚刚向我们展示的 updateItems方法
每当我的目录显示时 我都会呼叫updateItems 在过去 你可能使用的是onAppear 但在今年的SwiftUI中 请开始使用任务修改器 任务允许你将异步任务 与视图关联在一起 工作从视图的寿命开始工作 默认情况下是同步的 因此在闭包内 可以在myPhotos对象上 呼叫updateItems 并等待结果
这是使用任务的好方法 但这个修改器还能做到更多 任务的寿命与视图的寿命相关 因此你可以执行 诸如等待异步序列 和对它的值做出回应 当视图的使用寿命结束时 任务会自动取消 想了解视图寿命的更多情况 请务必收看座谈《SwiftUI一点就通》 (Demystify SwiftUI) 使用实时预览 我可以看到条目已更新 但我们仍然缺少美丽的图像 我已经更新了之前 科特说过的PhotoView 我会在标题后面添加一些背景材料 现在 让我们来添加图像 令人高兴的是 使用新的 异步影像API 从远程服务器加载影像 比以往都更容易 我需要做的是得到影像URL 我想取出我们的条目 并将其传递给异步影像
嗯 全尺寸有点太大 所以让我们使用异步影像的超载 让我能调整图像 并显示占位符 使用户知道他们的图像正在加载
接下来 我会让影像变成可调整尺寸 并设定其长宽比以填满空间
最后 我会添加最小宽度和高度 使我的图像具备弹性 使用不为零的最低高度 也能确保进度视图 能探出我的标题区域
与SwiftUI的其余部分一样 异步图像具有智能默认值 因此即使加载影像时出现错误 结果也会继续显示占位符 你还可以选择自定义错误处理行为 为了做到这点 请收看 《AsyncImage’s overload that uses a phase.》 如果用户 能把他们最爱的图片 存起来以稍后查看 那也很棒 让我们在这个标题区域加入按钮 来达到这一点 这个按钮会触发一个异步操作 来将影像条目储存到磁盘中 在应用中的“ 已储存” 页签中 就会出现储存的条目 我已经把视图stub进去 来做到这点 让我把它添加在这 然后我们可以看看它的代码
下面是我储存按钮的 stubbed-in版本 让我们新增动作来储存照片 SwiftUI中的按钮操作是同步的 但我的“储存”方法是异步的 要呼叫该方法 我将启动一个异步任务
然后 在闭包内 我会调用“照片”上的“储存”方法 这是异步的 所以我只会用await
我认为在储存期间 显示进度视图会很不错 为了做到这点 我会添加状态属性
再来 我将根据我的呼叫状态保存
然后 我将更新按钮上的标签 以便在保存进行时 显示进度视图 我使用“不透明”来隐藏储存标签 和叠加 来显示进度视图 这个组合可以 确保按钮维持在相同大小 根据“储存”这个字的本地化状况
最后我会在储存进行当下 关闭按钮功能
让我们看看它在实时预览中 是如何工作的
太棒了! 回到目录视图 把全部都整合在一起
SwiftUI今年有一个 很棒的新修改器 你可以使用它为大家 提供手动刷新数据的能力 通过往我的清单 加入可刷新的修改器 我会告诉SwiftUI 这个内容是可刷新的 我就能让一个异步的闭包 变得可刷新 并呼叫updateItems方法 来更新清单 如同我先前谈到的“任务” 我会在这个异步方法上 使用await
当我的异步工作完成时 刷新指示器会自动关闭 现在 我就能向下拉以刷新影像 点击“ 储存” 来储存我喜欢的影像 然后切换到“已储存”页签 以查看我保存的图像
Swift 的新功能 使处理并行性数据变得更容易 SwiftUI很好地 整合了Swift的并行性功能 并在默认选项下为你提供最佳行为 在大多数情况下 你只需要使用await 来利用并行性的力量 将ObservableObject 标记为“ @MainActor” 以便更有力地检查 对象是否以 与视图配合良好的方式更新
善用SwiftUI的API附件 花最效的力气 写出 安全又强效的并行性应用 使用异步影像来同时加载影像 将“可刷新”修改器添加 到视图层次架构中 以便用户手动刷新数据 就像我们在“储存”按钮上 看到的一样 你可以在你的自定义视图中 使用Swift的新并行功能
我们都知道 并行性很棘手 这是个难题 但有了这些新的语言功能 与SwiftUI API 现在你有了工具 让你在应用中管理这些复杂性 我们希望你们享受学习 Swift 5.5和SwiftUI中 很棒的新并行工具 期待看到你们 以各种方法使用它 解决应用中的棘手问题 [音乐]
-
-
1:55 - SpacePhoto
/// A SpacePhoto contains information about a single day's photo record /// including its date, a title, description, etc. struct SpacePhoto { /// The title of the astronomical photo. var title: String /// A description of the astronomical photo. var description: String /// The date the given entry was added to the catalog. var date: Date /// A link to the image contained within the entry. var url: URL } extension SpacePhoto: Codable { enum CodingKeys: String, CodingKey { case title case description = "explanation" case date case url } init(data: Data) throws { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(SpacePhoto.dateFormatter) self = try JSONDecoder() .decode(SpacePhoto.self, from: data) } } extension SpacePhoto: Identifiable { var id: Date { date } } extension SpacePhoto { static let urlTemplate = "https://example.com/photos" static let dateFormat = "yyyy-MM-dd" static var dateFormatter: DateFormatter { let formatter = DateFormatter() formatter.dateFormat = Self.dateFormat return formatter } static func requestFor(date: Date) -> URL { let dateString = SpacePhoto.dateFormatter.string(from: date) return URL(string: "\(SpacePhoto.urlTemplate)&date=\(dateString)")! } private static func parseDate( fromContainer container: KeyedDecodingContainer<CodingKeys> ) throws -> Date { let dateString = try container.decode(String.self, forKey: .date) guard let result = dateFormatter.date(from: dateString) else { throw DecodingError.dataCorruptedError( forKey: .date, in: container, debugDescription: "Invalid date format") } return result } private var dateString: String { Self.dateFormatter.string(from: date) } } extension SpacePhoto { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) description = try container.decode(String.self, forKey: .description) date = try Self.parseDate(fromContainer: container) url = try container.decode(URL.self, forKey: .url) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(description, forKey: .description) try container.encode(dateString, forKey: .date) } }
-
2:39 - Photos
/// The current collection of space photos. class Photos: ObservableObject { @Published private(set) var items: [SpacePhoto] = [] /// Updates `items` to a new, random list of photos. func updateItems() async { let fetched = fetchPhotos() items = fetched } /// Fetches a new, random list of photos. func fetchPhotos() -> [SpacePhoto] { let downloaded: [SpacePhoto] = [] for _ in randomPhotoDates() { } return downloaded } }
-
3:24 - CatalogView
struct CatalogView: View { @StateObject private var photos = Photos() var body: some View { NavigationView { List { ForEach(photos.items) { item in PhotoView(photo: item) .listRowSeparator(.hidden) } } .navigationTitle("Catalog") .listStyle(.plain) } } }
-
10:09 - Make fetch happen
/// An observable object representing a random list of space photos. @MainActor class Photos: ObservableObject { @Published private(set) var items: [SpacePhoto] = [] /// Updates `items` to a new, random list of `SpacePhoto`. func updateItems() async { let fetched = await fetchPhotos() items = fetched } /// Fetches a new, random list of `SpacePhoto`. func fetchPhotos() async -> [SpacePhoto] { var downloaded: [SpacePhoto] = [] for date in randomPhotoDates() { let url = SpacePhoto.requestFor(date: date) if let photo = await fetchPhoto(from: url) { downloaded.append(photo) } } return downloaded } /// Fetches a `SpacePhoto` from the given `URL`. func fetchPhoto(from url: URL) async -> SpacePhoto? { do { let (data, _) = try await URLSession.shared.data(from: url) return try SpacePhoto(data: data) } catch { return nil } } }
-
14:07 - CatalogView
struct CatalogView: View { @StateObject private var photos = Photos() var body: some View { NavigationView { List { ForEach(photos.items) { item in PhotoView(photo: item) .listRowSeparator(.hidden) } } .navigationTitle("Catalog") .listStyle(.plain) .refreshable { await photos.updateItems() } } .task { await photos.updateItems() } } }
-
15:11 - PhotoView with image
struct PhotoView: View { var photo: SpacePhoto var body: some View { ZStack(alignment: .bottom) { AsyncImage(url: photo.url) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { ProgressView() } .frame(minWidth: 0, minHeight: 400) HStack { Text(photo.title) Spacer() SavePhotoButton(photo: photo) } .padding() .background(.thinMaterial) } .background(.thickMaterial) .mask(RoundedRectangle(cornerRadius: 16)) .padding(.bottom, 8) } }
-
18:06 - SavePhotoButton
struct SavePhotoButton: View { var photo: SpacePhoto @State private var isSaving = false var body: some View { Button { Task { isSaving = true await photo.save() isSaving = false } } label: { Text("Save") .opacity(isSaving ? 0 : 1) .overlay { if isSaving { ProgressView() } } } .disabled(isSaving) .buttonStyle(.bordered) } }
-
20:28 - CatalogView
struct CatalogView: View { @StateObject private var photos = Photos() var body: some View { NavigationView { List { ForEach(photos.items) { item in PhotoView(photo: item) .listRowSeparator(.hidden) } } .navigationTitle("Catalog") .listStyle(.plain) .refreshable { await photos.updateItems() } } .task { await photos.updateItems() } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。