SwiftUI 系列 10|如何处理异步加载:Task、async/await 和界面状态联动
真正难的不是把请求发出去,而是让 loading、结果、错误和页面生命周期保持一致
很多 SwiftUI 页面一接上异步加载,问题就开始变多:
- 为什么
onAppear会重复请求 - 为什么 loading 状态不稳定
- 为什么旧结果会把当前页面改回去
- 为什么页面离开后任务还在回写
这些问题表面看像 Task 或 async/await 的问题,但更根本的原因往往是:
异步任务生命周期和页面状态生命周期没有对齐。
所以真正要处理的不是“怎么发请求”,而是:
- 请求什么时候开始
- 结果回来后有没有资格改页面
- 旧任务什么时候该失效
- loading / error / content 这些状态之间如何互斥
一、最容易踩的坑不是不会写 await,而是把页面异步当成“一次请求 + 一个布尔值”
很多页面最初都会写成这样:
- 一个
isLoading - 一个
items - 一个
errorMessage
再配一个:
Task {
await load()
}
刚开始当然能跑,但只要页面开始支持:
- 重试
- 下拉刷新
- 搜索
- 条件切换
问题就会接连出现。
因为真实页面不是“一次请求”,而是一组可能互相替换、互相覆盖的任务。
二、异步页面真正的核心,不是请求,而是状态流
如果你把异步加载只理解成“把数据拿回来”,会很容易忽略页面真正关心的是这些状态:
- 初次进入时的加载
- 已有内容基础上的刷新
- 加载失败后的重试
- 某些条件变化后的重新请求
这些都是“加载”,但它们在页面语义上不是一回事。
如果全都只靠一个 isLoading 去表示,页面很快就会乱。
所以更实用的思路通常是:
- 先定义页面当前处于什么语义状态
- 再让异步任务去驱动这个状态流变化
三、为什么 onAppear + Task 经常会越写越危险
因为它太顺手了。
很多人会自然写出:
.onAppear {
Task {
await viewModel.load()
}
}
这段代码本身不一定错,但危险在于它太容易让你默认:
- 页面出现一次
- 请求发一次
- 返回后改一次状态
而真实项目里,这三个假设都可能不成立。
所以异步加载真正稳不稳,关键不在于有没有 Task,而在于:
- 同类任务是否被统一管理
- 旧任务是否会取消或失效
- 结果是否需要校验当前上下文
四、一个常见误区:结果成功就直接落页面
很多页面状态错乱,根源都在这里。
比如:
- 关键词已经变了
- 旧请求晚一点回来
- 旧结果仍然把页面改掉
从请求角度看它没有失败,但从页面角度看它已经过期了。
所以异步页面里非常重要的一件事是:
不是所有成功返回的结果,都自动有资格回写当前 UI。
这也是为什么页面异步加载通常不能只看“有没有拿到结果”,还必须看“这个结果现在是否仍然有效”。
五、一个更稳的方向:把加载任务收口,而不是让每个 UI 事件自己发请求
如果一个页面支持:
- 初次加载
- 重试
- 刷新
- 筛选变化
那更稳的做法通常不是每个入口都各自开一个 Task,而是把同类任务统一收口。
例如:
- 所有入口最终都调用同一个
reload() - 当前活跃任务由 ViewModel 持有
- 新任务出现时,旧任务被取消或判定失效
这样页面状态至少会更容易保持一致,因为:
- 谁在加载是清楚的
- 谁能结束 loading 也是清楚的
六、结论:SwiftUI 异步加载真正要处理的,是任务和页面状态能不能说得清
如果只用一句话总结,我会说:
SwiftUI 里处理异步加载,真正难的不是把请求发出去,而是让任务生命周期、结果有效性和页面状态流始终保持一致。
只要这三件事没立住,页面就会不断长出:
- 重复请求
- 旧结果回写
- loading 错乱
而这些问题,通常不是多写几个 Task 就能补好的。