
-
跟着视频学编程:使用 Swift 并发机制提升 App 性能
通过更新一个现有的示例 App,我们将向你介绍如何通过 Swift 并发机制来优化 App 的用户体验。我们将从一个主 Actor App 入手,然后根据需要逐步引入异步代码。我们将使用任务来优化主 Actor 上运行的代码,并探索如何通过将工作转移到后台来实现代码并行运行。我们将探讨数据争用安全机制提供的功能,并讲解如何解读和修复数据争用安全错误。最后,我们将展示如何在 App 情境中充分利用结构化并发机制。
章节
- 0:00 - 简介
- 2:11 - Approachable Concurrency 配置
- 2:51 - 示例 App 架构
- 3:42 - 从图库异步载入照片
- 9:03 - 从照片提取贴纸和颜色
- 12:30 - 在后台线程上运行任务
- 15:58 - 并行执行任务
- 18:44 - 利用 Swift 6 防止数据争用
- 27:56 - 利用结构化并发机制控制异步代码
- 31:36 - 总结
资源
相关视频
WWDC25
WWDC23
-
搜索此视频…
大家好! 我是 Sima 从事 Swift 和 SwiftUI 的开发工作 在这个视频中 你将学习如何 通过 Swift 并发提升 App 性能 作为 App 开发者 我们编写的 大部分代码都运行在主线程上
单线程代码易于理解和维护 但现代 App 往往需要执行耗时任务 比如网络请求或复杂计算 这时 将工作移出主线程 以保持 App 响应 就显得尤为重要 Swift 为你提供了编写 可靠并发代码的全套工具 在本讲座中 我将与你一起 构建 App 演示具体实现方式 我们将从一个单线程 App 开始 根据需要逐步引入异步代码 然后通过将耗时任务转移到后台 并行执行来优化 App 性能 接下来 我们会探讨几种 你可能遇到的常见的数据争用 安全场景及应对方案 最后我将介绍结构化并发 并演示如何使用 TaskGroup 等工具更好地 控制并发代码 现在就开始吧! 我非常喜欢写日记 并用贴纸装饰日记内容 因此我将带大家 开发一个能将任意照片 组合成贴纸包的 App 我们的 App 主要包含两个视图 第一个视图将展示 所有贴纸的轮播效果 每个贴纸都会带有基于 原照片颜色的渐变背景 第二个视图则以网格形式 预览整个贴纸包 支持直接导出 欢迎下载下面的 示例 App 来跟着一起操作! 创建项目时 Xcode 启用了多项功能 来降低并发编程的入门门槛 包括默认主 Actor 模式 和一些即将推出的功能 这些功能在 Xcode 26 的 新 App 项目中会默认开启
在渐进式并发配置下 Swift 6 语言模式会在你准备好之前 无需引入并发代码 即可保障数据争用安全 如果你想在现有项目中启用这些功能 可以参考 Swift 迁移指南
代码层面 App 主要包含两个视图: StickerCarousel 和 StickerGrid 这些视图会使用 PhotoProcessor 结构体 负责提取的贴纸数据
PhotoProcessor 会先从照片库 获取原始图像 随后生成贴纸
StickerGrid 视图包含一个 ShareLink 用于分享贴纸包
PhotoProcessor 类型 执行了两项耗时操作: 贴纸提取和主色调计算 我们来看看 Swift 并发功能 如何帮助我们在 保证流畅用户体验的同时 高效执行这些耗时任务! 现在我们从 StickerCarousel 视图开始 这个视图通过水平滚动视图展示贴纸 滚动视图内部 使用 ForEach 遍历 视图模型中存储的 从照片库选中的照片数组 它会检查 viewModel 中的 processedPhotos 字典 以获取与照片库选中项对应的 已处理照片 当前 我们没有任何经过处理的照片 因为我们还没有编写 从照片选择器获取图像的实际代码 如果我现在运行这个 App 我们在滚动视图中看到的只有 StickerPlaceholder 视图 我会通过按住 Command 同时点按 跳转到 StickerViewModel StickerViewModel 存储了 当前从照片库中 选中的照片数组 这些照片以 SelectedPhoto 类型表示 我会按住 Option 并点按打开 “快速帮助”来进一步了解这种类型
SelectedPhoto 属于 Identifiable 类型 它存储了 PhotosUI 框架的 PhotosPickerItem 及关联的 ID 这个模型还包含一个名为 processedPhotos 的字典 它将已选照片的 ID 映射到 对应的 SwiftUI Image 视图 我已经开始编写 loadPhoto 函数 来处理选中的照片 但目前它尚未从存储的 照片选择器项加载任何数据 PhotosPickerItem 遵循 SDK 中的 Transferable 协议 这个协议允许我使用异步 loadTransferable 函数 加载所需的表示形式 我将请求 Data 表示形式
现在 我们遇到了一个编译错误
这是因为 loadTransferable 是一个异步调用 而当前 loadPhoto 函数 并未设置为支持异步操作 于是 Swift 建议我用 async 关键字标记 loadPhoto 我决定采纳这个建议
现在 我们的函数能够处理 异步代码了 但这里还有一个错误 虽然 loadPhoto 可以处理异步调用 但仍需明确指定它需要等待哪个操作 为此 我需要在调用 loadTransferable 时 添加 await 关键字 我将采纳这个修复建议
我将在 StickerCarousel 视图中 调用这个函数 通过 Command-Shift-O 我可以 使用 Xcode 的“快速打开”功能 跳转回 StickerCarousel 视图
我计划在 StickerPlaceholder 视图出现时调用 loadPhoto 函数 由于这是一个异步函数 我会使用 SwiftUI 的 task 修饰符 在视图出现时启动照片处理任务
让我们在设备上测试一下!
很好 它正常运行了 我试着选几张照片来验证一下
太好了!照片成功从照片库加载 通过 task 修饰符 App 在后台加载图片数据时 App 用户界面依然保持流畅响应 此外 由于我使用 LazyHStack 来展示图片 系统只会为屏幕上需要渲染的 视图触发照片加载任务 这样 App 就避免了 不必要的性能开销 现在我们聊聊 async/await 如何提升 App 响应能力
我们在调用 loadTransferable 方法时添加了 await 关键字 并将 loadPhoto 函数标记为 async 这个 await 关键字标识了 一个可能的暂停点 这意味着 loadPhoto 函数 最初在主线程启动 当执行到 await 处的 loadTransferable 调用时 函数会暂停执行 等待 loadTransferable 完成 在 loadPhoto 暂停期间 Transferable 框架会在 后台线程执行 loadTransferable 任务 当 loadTransferable 完成后 loadPhoto 会重新在 主线程恢复执行并更新图片 当 loadPhoto 暂停时 主线程可以继续处理 UI 事件和其他任务 await 关键字标识了一个 代码位置:在函数暂停期间 允许执行其他工作 就这样 我们完成了 从照片库加载图片的功能! 在这一过程中 我们已经理解了 异步代码的含义及编写思路 现在 让我们为 App 添加代码 以从照片中提取贴纸 获取照片主色调用于 轮播视图的背景渐变
按住 Command 键同时点按 跳转回 loadPhoto 方法 准备添加这些特效处理
项目中已内置 PhotoProcessor 组件 它能接收 Data 数据 提取颜色和贴纸 最终返回处理后的照片对象 我不会直接根据数据生成基础图像 而是改用 PhotoProcessor 来处理
由于 PhotoProcessor 返回的是处理后的照片 我需要更新字典类型
这个 ProcessedPhoto 类型将提供 从原图提取的贴纸 以及用于构建渐变背景的颜色数组
我已在项目中添加 GradientSticker 视图来接收 processedPhoto 我现在通过“快速打开”来找到它
这个视图通过 ZStack 将处理后的照片中的贴纸 叠加在线性渐变背景上显示
现在我要把这个 GradientSticker 添加到轮播视图中
当前 StickerCarousel 只是简单调整照片尺寸 但既然已经有了处理后的照片 我们可以转而使用 GradientSticker
让我们构建并运行 App 来看看我们的贴纸!
成功了!
哦不! 在提取贴纸的过程中 轮播视图的滚动不太流畅
我怀疑图像处理的开销太大了 我已使用 Instruments 对 App 进行分析验证确认了这一点 追踪数据显示 App 出现了严重卡顿
放大检查最耗时的堆栈轨迹时 可以清楚地看到照片处理器 正在主线程执行 耗时超过 10 秒的繁重处理任务! 若想深入了解如何分析 App 卡顿问题 请观看我们的讲座 “使用 Instruments 分析挂起” 现在 让我们详细讨论一下 App 在主线程上的工作负载问题
loadTransferable 的实现已通过 将任务转移到后台线程 避免了加载工作阻塞主线程
但我们新增的图像处理代码 仍在主线程运行 这些耗时操作会完全阻塞主线程 导致滚动手势等 UI 交互 无法及时响应 严重影响用户体验
之前我们采用的 SDK 异步 API 已自动实现了任务分流处理 现在我们需要通过 并行执行代码来解决卡顿问题 可以将部分图像转换操作移至后台 图像转换包含这三个操作步骤 获取原始图像和更新图像 需要与用户界面交互 因此这部分工作无法移至后台进行 但我们可以将图像处理任务 转移到后台执行 这样既能保证繁重的 图像处理工作顺利进行 又能让主线程及时响应其他事件 让我们来看看 PhotoProcessor 结构体的实现方案!
由于我的 App 默认 处于主 Actor 模式 PhotoProcessor 被标记为 @MainActor 这意味着所有方法 都必须在主线程执行 process 方法会调用 extract sticker 和 extract colors 方法 因此我需要将这个类型的所有方法 标记为可在非主 Actor 执行 为此 我可以用 nonisolated 标记 整个 PhotoProcessor 类型 这是 Swift 6.1 中引入的 一项新功能 当类型被标记为 nonisolated 时 它所有的属性和方法 都会自动获得非隔离状态
现在 PhotoProcessor 已解除 与 MainActor 的绑定 我们可以为 process 函数 添加新的 @concurrent 属性 并标记为 async 这将告知 Swift 在调用这个方法时 始终切换到后台线程执行 我将通过“快速打开” 返回 PhotoProcessor
首先我会在类型上应用 nonisolated 来解除 PhotoProcessor 与主 Actor 的绑定 使它的方法 能够被并发代码调用
现在 PhotoProcessor 已解除隔离 为确保 process 方法 能在后台线程调用 我将添加 @concurrent 和 async
现在 我将通过“快速打开” 跳转回 StickerViewModel
在 loadPhoto 方法中 Swift 建议我 通过使用 await 关键字 调用 process 方法 来让代码脱离主线程运行 我将直接采纳这个建议
我们来构建并运行 App 看看将这些工作移出主 Actor 是否解决了卡顿问题!
看起来滚动时不再卡顿了!
不过 虽然现在 UI 可以流畅交互 但在滚动过程中图像仍 需要较长时间才能显示出来 保持 App 的响应速度只是 提升用户体验的一个方面 即使我们将工作移出主线程 但如果用户需要 等待很久才能看到结果 这种体验依然会让人沮丧
我们确实把图像处理操作 转移到了后台线程 但整个处理过程仍然耗时过长 让我们看看如何通过并发优化 来加速这个操作 图像处理需要完成 贴纸提取和主色识别两个任务 但这两个操作彼此独立 因此 我们可以使用 async let 让它们并行执行 这样 管理所有后台线程的 并发线程池 就会立即将这两个任务 调度到不同的后台线程上运行 从而充分利用手机的多核处理能力
要采用 async let 可按住 Command 同时点按进入 process 方法
按住 Control + Shift 和向下箭头 键启用多行光标 在 sticker 和 colors 变量前 添加 async 修饰符
现在我们已经让这两个调用并行执行 接下来需要 等待它们的结果 才能继续执行 process 函数 我们用 Editor 菜单来修复这些问题
不过 还有一个错误需要解决 这次是关于数据争用的! 先来了解一下这个错误
这个错误意味着 PhotoProcessor 类型 无法安全地在并发任务间共享 要理解原因 我们需要 查看它的存储属性 PhotoProcessor 唯一持有的属性 是一个用于从照片提取颜色的 ColorExtractor 实例 ColorExtractor 类用于计算 图像中的主色调 这种计算基于包括像素缓冲区在内的 底层可变的图像数据进行处理 因此这个颜色提取器类型 不支持并发访问
当前所有颜色提取操作 都共享同一个 ColorExtractor 实例 这会导致对同一块内存的并发访问
即所谓的“数据争用” 这种情况可能引发运行时错误 如崩溃或不可预测的行为 Swift 6 语言模式会在 编译阶段就识别这些问题 从根本上消除并行代码中的这类错误 将原本难以追踪的运行时错误 转化为可即时修复的编译错误 若点击错误信息中的“帮助”按钮 你可在 Swift 网站上 查看这个错误的详细说明 解决数据争用问题时 有多种方案可以考虑 具体选择取决于 代码对共享数据的使用方式 首先需明确 这些可变状态是否 需要在并发代码间共享? 多数情况下 你完全可以避免共享 但在某些情况下 状态需要由这类代码共享 如果是这样 请考虑将 需要共享的部分提取为 可安全传递的值类型 仅当这些方案均不适用时 再考虑将状态隔离至某个 Actor 如 MainActor 让我们看看第一种方案 是否适用于当前场景! 虽然可以重构这个类型 让它以不同的方式 处理多个并发操作 但我们可以将颜色提取器 改为 extractColors 函数中的 局部变量 这样每张照片的处理 都会拥有独立的提取器实例 这一修改是正确的 因为颜色提取器本身设计为 单次仅处理一张照片 因此我们想为每个色彩提取任务 创建独立实例 修改后 extractColors 函数外部 无法访问颜色提取器 从而避免了数据争用问题!
具体方式是将颜色提取器属性 移至 extractColors 函数内
太棒了! 在编译器的帮助下 我们成功检测并修复了 App 中的数据争用问题 现在 运行程序试试吧!
能明显感觉到 App 运行变快了!
如果在 Instruments 中 收集性能跟踪并打开分析 会发现卡顿现象已经消失 现在让我们快速回顾 通过 Swift 并发实现的优化! 通过采用 @concurrent 属性 我们成功将图像处理代码移出主线程 同时使用 async let 实现了贴纸生成 与色彩提取的并行操作 大幅提升了 App 性能 基于 Swift 并发的优化应当始终 以分析工具 如 Time Profiler 的数据为依据 如果能在不引入并发的情况下 提升代码效率 就应该优先采用 现在 App 的运行体验流畅无比! 让我们暂时放下图像处理 来点有趣的!
现在要为处理好的贴纸添加视觉特效 让贴纸在滚动离开时 逐渐淡出并模糊 这就切换到 Xcode 开始编码吧!
我将通过 Xcode 项目导航器 回到 StickerCarousel
现在我要用 visualEffect 修饰器 为滚动视图中的 每张图片添加特效
这里需要对视图应用三种效果 我想更改滚动视图中 最后一张贴纸的偏移量、 模糊度和不透明度 因此需要访问 viewModel 的 selection 属性 来判断当前是否处于最后一张贴纸
似乎出现了编译错误 因为我试图 从 visualEffect 闭包中访问 被主 Actor 保护的视图状态 由于视觉特效的计算开销较大 SwiftUI 会将它移出主线程 以确保 App 性能达到最佳
如果想深入了解这个机制 可以观看我们的讲座 “探索 SwiftUI 中的并发机制” 其实这个错误正是在提醒我: 这个闭包稍后会在后台线程执行 按住 Command 键同时点按 来查看 visualEffect 的定义 以确认这一点
在定义中 这个闭包 被标记为 @Sendable 这正是 SwiftUI 表明 这个闭包将在后台执行的明确信号
每当 selection 发生变化时 SwiftUI 都会重新调用特效闭包 因此我们可以通过 闭包捕获列表为它创建副本
这样当 SwiftUI 调用这个闭包时 操作的就是 selection 的副本值 从而彻底避免数据争用
现在来看看我们的视觉效果吧!
效果非常出色! 可以看到在滚动时 前一张图片会逐渐模糊淡出
我们遇到的这两个数据争用案例 解决方案都是避免在 并发代码中共享可变数据 关键区别在于 第一个案例是 我主动并行执行代码时 引入的数据争用 第二个案例是使用 SwiftUI API 时 系统自动 将任务分流到后台线程所致
若必须共享可变状态 还有其他保护机制 Sendable 值类型能防止 这个类型被并发代码共享 比如当 extractSticker 和 extractColors 方法并行处理 同一图片数据时 由于 Data 是 Sendable 值类型 完全不会引发数据争用 Data 类型还实现了写时复制机制 因此仅在数据被修改时才会拷贝 如果无法使用值类型 可以考虑 将状态隔离到主 Actor 幸运的是 默认的主 Actor 模式 已经为你实现了这种隔离 例如 虽然我们的模型是类 但仍可从并发任务访问它 由于模型已隐式标记为 MainActor 从并发代码中引用是安全的 代码需要切换到主 Actor 才能访问状态 在这个例子中 虽然类 受主 Actor 的保护 但这一原则同样适用于 代码中可能存在的其他 Actor 目前 App 效果看起来不错! 但还不够完善
为了能够导出贴纸 我们需要添加一个贴纸网格视图 它会为每张尚未处理的照片 启动处理任务 并一次性显示所有贴纸 同时还将添加一个分享按钮 支持将这些贴纸导出 让我们回到代码部分!
首先 按住 Command 同时点按 跳转到 StickerViewModel
我打算给模型添加一个 新方法 processAllPhotos()
这个方法需要遍历模型中 已保存的所有已处理照片 如果还有未处理的照片 就启动多个并行任务 来同时处理它们
之前我们使用过 async let 但那仅限于我们知道 只有贴纸提取和颜色提取 这两个任务的情况 现在 我们需要为数组中的 所有原始照片创建新任务 而这些处理任务的数量可能是任意的
像 TaskGroup 这样的 API 能让你更精细地控制 App 需要执行的异步任务
任务组提供了对子任务及结果的 细粒度管理能力 它可以启动任意数量的并行子任务
每个子任务的完成时间可能各不相同 因此它们的完成顺序也是不确定的 在我们的场景中 处理完成的照片会被存入字典 所以顺序无关紧要
由于 TaskGroup 遵循 AsyncSequence 协议 我们可以遍历已完成的任务结果 并将它们存入字典 最后 我们可以通过 await 等待整个任务组完成所有子任务 现在让我们回到代码 开始改造任务组! 首先 我需要声明一个任务组
在这个闭包中 我持有对任务组的引用 可随时向任务组添加图像处理任务 接下来我将遍历 模型中存储的选中照片
若某张照片已完成处理 则无需再创建任务
否则就启动一个新任务 来加载数据并处理照片
由于任务组是异步序列 我可以遍历它 将处理好的照片 存入 processedPhotos 字典
搞定! 现在可以准备在 StickerGrid 中 展示贴纸了
我将通过“快速打开” 跳转到 StickerGrid
这里有一个状态属性 finishedLoading 用于标记所有照片是否已完成处理
若处理未完成 界面会显示进度条 现在我要调用刚实现的 processAllPhotos() 方法
待所有照片处理完成后更新状态变量 最后 我还会在工具栏添加分享链接 来分享贴纸!
分享链接项会为每张 选中照片生成对应的贴图
让我们运行 App 吧!
我会轻点“StickerGrid”按钮 得益于 TaskGroup 预览网格 能立即开始并行处理所有照片 当处理完成后 所有贴纸都会瞬间呈现! 最后 通过工具栏的“分享”按钮 我可以将所有贴纸导出为 可保存的文件
在我们的 App 中 贴纸会 按处理完成的顺序收集 当然 你也可以自行跟踪顺序 任务组的功能远不止于此 更多详情请观看讲座 “超越结构化并发的基础”
恭喜! App 大功告成 可以尽情保存贴纸啦! 我们为 App 新增了多项功能 这些更新在 UI 上有着显著体现 为了提升响应速度与运行性能 我们充分运用了并发技术 还了解了结构化并发机制 以及如何避免数据争用问题
如果你没有跟着一起编写代码 仍然可以下载最终版 App 用你自己的照片制作专属贴纸! 要熟练掌握本次分享中提到的 Swift 并发新特性与技巧 可以尝试进一步 优化和调整这个 App 最后 不妨将这些技术 应用到你自己的 App 中 切记先进行性能分析! 若想更深入理解 Swift 并发模型的概念 推荐观看我们的讲座 “采用 Swift 并发” 如需将现有项目迁移至 这套易用的新的并发系统 请参考“Swift 迁移指南”! 最开心的是 现在可以 为笔记本贴上专属贴纸啦! 感谢观看!
-
-
6:29 - Asynchronously loading the selected photo from the photo library
func loadPhoto(_ item: SelectedPhoto) async { var data: Data? = try? await item.loadTransferable(type: Data.self) if let cachedData = getCachedData(for: item.id) { data = cachedData } guard let data else { return } processedPhotos[item.id] = Image(data: data) cacheData(item.id, data) }
-
6:59 - Calling an asynchronous function when the SwiftUI View appears
StickerPlaceholder() .task { await viewModel.loadPhoto(selectedPhoto) }
-
9:45 - Synchronously extracting the sticker and the colors from a photo
func loadPhoto(_ item: SelectedPhoto) async { var data: Data? = try? await item.loadTransferable(type: Data.self) if let cachedData = getCachedData(for: item.id) { data = cachedData } guard let data else { return } processedPhotos[item.id] = PhotoProcessor().process(data: data) cacheData(item.id, data) }
-
9:56 - Storing the processed photo in the dictionary
var processedPhotos = [SelectedPhoto.ID: ProcessedPhoto]()
-
10:45 - Displaying the sticker with a gradient background in the carousel
import SwiftUI import PhotosUI struct StickerCarousel: View { @State var viewModel: StickerViewModel @State private var sheetPresented: Bool = false var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: 16) { ForEach(viewModel.selection) { selectedPhoto in VStack { if let processedPhoto = viewModel.processedPhotos[selectedPhoto.id] { GradientSticker(processedPhoto: processedPhoto) } else if viewModel.invalidPhotos.contains(selectedPhoto.id) { InvalidStickerPlaceholder() } else { StickerPlaceholder() .task { await viewModel.loadPhoto(selectedPhoto) } } } .containerRelativeFrame(.horizontal) } } } .configureCarousel( viewModel, sheetPresented: $sheetPresented ) .sheet(isPresented: $sheetPresented) { StickerGrid(viewModel: viewModel) } } }
-
14:13 - Allowing photo processing to run on the background thread
nonisolated struct PhotoProcessor { let colorExtractor = ColorExtractor() @concurrent func process(data: Data) async -> ProcessedPhoto? { let sticker = extractSticker(from: data) let colors = extractColors(from: data) guard let sticker = sticker, let colors = colors else { return nil } return ProcessedPhoto(sticker: sticker, colorScheme: colors) } private func extractColors(from data: Data) -> PhotoColorScheme? { // ... } private func extractSticker(from data: Data) -> Image? { // ... } }
-
15:31 - Running the photo processing operations off the main thread
func loadPhoto(_ item: SelectedPhoto) async { var data: Data? = try? await item.loadTransferable(type: Data.self) if let cachedData = getCachedData(for: item.id) { data = cachedData } guard let data else { return } processedPhotos[item.id] = await PhotoProcessor().process(data: data) cacheData(item.id, data) }
-
20:55 - Running sticker and color extraction in parallel.
nonisolated struct PhotoProcessor { @concurrent func process(data: Data) async -> ProcessedPhoto? { async let sticker = extractSticker(from: data) async let colors = extractColors(from: data) guard let sticker = await sticker, let colors = await colors else { return nil } return ProcessedPhoto(sticker: sticker, colorScheme: colors) } private func extractColors(from data: Data) -> PhotoColorScheme? { let colorExtractor = ColorExtractor() return colorExtractor.extractColors(from: data) } private func extractSticker(from data: Data) -> Image? { // ... } }
-
24:20 - Applying the visual effect on each sticker in the carousel
import SwiftUI import PhotosUI struct StickerCarousel: View { @State var viewModel: StickerViewModel @State private var sheetPresented: Bool = false var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: 16) { ForEach(viewModel.selection) { selectedPhoto in VStack { if let processedPhoto = viewModel.processedPhotos[selectedPhoto.id] { GradientSticker(processedPhoto: processedPhoto) } else if viewModel.invalidPhotos.contains(selectedPhoto.id) { InvalidStickerPlaceholder() } else { StickerPlaceholder() .task { await viewModel.loadPhoto(selectedPhoto) } } } .containerRelativeFrame(.horizontal) .visualEffect { [selection = viewModel.selection] content, proxy in let frame = proxy.frame(in: .scrollView(axis: .horizontal)) let distance = min(0, frame.minX) let isLast = selectedPhoto.id == selection.last?.id return content .hueRotation(.degrees(frame.origin.x / 10)) .scaleEffect(1 + distance / 700) .offset(x: isLast ? 0 : -distance / 1.25) .brightness(-distance / 400) .blur(radius: isLast ? 0 : -distance / 50) .opacity(isLast ? 1.0 : min(1.0, 1.0 - (-distance / 400))) } } } } .configureCarousel( viewModel, sheetPresented: $sheetPresented ) .sheet(isPresented: $sheetPresented) { StickerGrid(viewModel: viewModel) } } }
-
26:15 - Accessing a reference type from a concurrent task
Task { @concurrent in await viewModel.loadPhoto(selectedPhoto) }
-
29:00 - Processing all photos at once with a task group
func processAllPhotos() async { await withTaskGroup { group in for item in selection { guard processedPhotos[item.id] == nil else { continue } group.addTask { let data = await self.getData(for: item) let photo = await PhotoProcessor().process(data: data) return photo.map { ProcessedPhotoResult(id: item.id, processedPhoto: $0) } } } for await result in group { if let result { processedPhotos[result.id] = result.processedPhoto } } } }
-
30:00 - Kicking off photo processing and configuring the share link in a sticker grid view.
import SwiftUI struct StickerGrid: View { let viewModel: StickerViewModel @State private var finishedLoading: Bool = false var body: some View { NavigationStack { VStack { if finishedLoading { GridContent(viewModel: viewModel) } else { ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } } .task { await viewModel.processAllPhotos() finishedLoading = true } .toolbar { ToolbarItem(placement: .topBarTrailing) { if finishedLoading { ShareLink("Share", items: viewModel.selection.compactMap { viewModel.processedPhotos[$0.id]?.sticker }) { sticker in SharePreview( "Sticker Preview", image: sticker, icon: Image(systemName: "photo") ) } } } } .configureStickerGrid() } } }
-