Swift Concurrency 系列 07|SwiftUI 和 async/await 结合时最常见的坑
真正的坑通常不在语法,而在于你有没有把“页面生命周期”和“任务生命周期”对齐
SwiftUI 和 async/await 单独看都很优雅,但它们一结合,最容易暴露的问题恰恰不是语法,而是生命周期。
更准确地说,是两个生命周期之间的错位:
- SwiftUI 页面什么时候出现、重绘、消失
- 异步任务什么时候开始、暂停、结束、取消
如果你没有把这两件事对齐,代码即使今天能跑,后面也很容易长出:
- 重复请求
- 页面闪动
- 旧结果回写
- loading 状态错乱
- 页面离开后任务还在更新状态
所以这篇文章真正想讲的是:SwiftUI 异步页面为什么容易乱,以及这些乱背后的根源是什么。
一、最常见的误判:把 View 当成稳定对象
这是很多 UIKit 背景开发者最容易带进来的习惯。
大家会默认:
- 页面出现一次
- 请求发一次
- 请求回来后更新当前页面
但 SwiftUI 里的 View 更像状态描述,而不是一个你可以长期抓着不放的稳定实例。
这意味着如果你脑子里默认的是“我眼前这个 View 会一直在,所以这个任务自然属于它”,后面就很容易出问题。
SwiftUI 没有要求你把任务绑在一个稳定对象上,它只是很容易让你误以为自己已经这么做了。
二、onAppear 最大的坑,不是会重复触发,而是你把它当成了一次性初始化入口
很多文章会说:onAppear 可能执行多次。
这句话没错,但还不够。
真正危险的地方在于,很多人把它写成了“页面初始化”的事实标准入口:
.onAppear {
Task {
await loadData()
}
}
问题不在这段代码一定错,而在于你很容易在心智上偷偷加了一句:
“这个页面出现时,只会跑这一遍。”
一旦你这么想,后面就会接连出现:
- 重复请求
- 重复重置状态
- 重复埋点
- 数据刚展示好又被清掉重来
所以更稳的思路不是“想办法让 onAppear 只触发一次”,而是:
让你的异步流程本身具备幂等、去重或可替换能力。
三、第二个坑:把页面状态和任务状态混在一起
一个 SwiftUI 页面里常见的状态包括:
- 当前筛选条件
- 当前数据内容
- 是否加载中
- 错误信息
- 当前正在跑的任务
如果这些状态没有被明确分层,就很容易混成一团。
最常见的坏味道是:
itemsisLoadingerrorisRefreshingkeywordselectedTab
每个值单独都合理,但你很难说清它们彼此之间是什么关系。
于是页面就会进入一种很别扭的状态:
- 看起来什么状态都有
- 但没有一个状态真正表达了“页面现在处于什么语义”
在这种情况下,异步结果一回来,任何状态都可能被改一把,问题只是早晚暴露。
四、第三个坑:旧结果回写当前 UI
这是 SwiftUI 异步页面里最高频的问题之一。
典型场景包括:
- 用户快速切换 tab
- 搜索关键词连续变化
- 筛选条件反复切换
- 页面先后触发刷新和首次加载
你以为自己只是“发了多个任务”,其实真正的问题是:
旧任务虽然还合法完成了,但它已经不再对应当前页面状态。
一旦旧结果仍然能写当前 UI,你看到的表象通常只是:
- 页面闪一下
- 列表突然回退
- loading 状态突然结束
- 错误提示莫名弹出来
这些现象很像“小 bug”,但根源非常一致:
任务结果的有效性没有被管理。
五、第四个坑:把所有异步入口都摊在 View 上
一个页面如果同时出现这些入口:
onAppear { Task { ... } }refreshable { await ... }onChange(of:) { Task { ... } }- 按钮点击里再开一个
Task
它们单独看都很合法,但放在一起就会迅速变成一个问题:
任务关系失控。
你会越来越难回答:
- 谁是主入口
- 谁应该取消谁
- 谁才有资格改当前展示状态
- 某个状态更新到底对应哪轮任务
所以很多 SwiftUI 异步页面不是不会写,而是入口太多,每个入口都能直接触发任务,最后没有统一协调层。
六、第五个坑:默认“只要能更新 UI 就行”
SwiftUI 把很多 UI 更新细节隐藏掉了,这会让人产生一种错觉:只要我最后把状态改掉,页面自然会刷新。
但真实问题不在“会不会刷新”,而在“有没有资格在这个时机刷新”。
比如:
- 当前结果是不是已经过期
- 当前页面是不是还活着
- 当前状态是不是仍然匹配这轮任务
- 当前修改是不是应该在主 actor 语义下进行
如果这些事情没有被认真对待,页面可能不会立刻崩,但会慢慢累积出一堆“偶发错乱”。
七、更稳的做法:让 View 触发意图,让状态层管理任务
我更推荐的组织方式是:
- View 负责表达用户意图
- ViewModel 或状态层负责管理任务和结果资格
- View 只消费已经整理好的状态
也就是说,View 最好少知道这些细节:
- 是否取消旧任务
- 哪个结果已经过期
- 当前 loading 是首次加载还是刷新
- 错误状态是否应该覆盖旧内容
这些问题如果都留在 View 上,页面很快就会从“声明式 UI”变成“声明式外壳包着一层分散异步逻辑”。
八、一个更接近实战的建议
如果一个 SwiftUI 页面已经开始变复杂,我通常会强迫自己先回答下面几个问题:
- 页面有哪些任务入口。
- 同类任务之间是并存、替换还是忽略。
- 哪些状态是页面语义状态,哪些只是内部过程状态。
- 旧结果是否还允许改当前页面。
- 页面离开后,还有哪些任务应该继续,哪些应该停止。
这几个问题一旦答不上来,通常说明不是 API 用法有问题,而是页面任务模型还没立住。
九、结论:SwiftUI 异步页面真正难的,是生命周期对齐
很多人以为 SwiftUI 和 async/await 的坑主要在语法上。
但更真实的情况是:
View不是你以为的稳定对象onAppear不是一次性初始化语义- 旧结果不会因为“已经过期”就自动失效
- 页面层如果任务入口太多,就一定会开始乱
所以真正稳的 SwiftUI 异步页面,不是“哪里能开任务就开哪里”,而是:
先把页面生命周期和任务生命周期对齐,再去写具体异步代码。
只有这件事先成立,SwiftUI 的轻量和 async/await 的优雅,才会真正变成优势,而不是把混乱写得更顺手。