返回文章列表

SwiftUI 系列 10|如何处理异步加载:Task、async/await 和界面状态联动

真正难的不是把请求发出去,而是让 loading、结果、错误和页面生命周期保持一致

很多 SwiftUI 页面一接上异步加载,问题就开始变多:

  • 为什么 onAppear 会重复请求
  • 为什么 loading 状态不稳定
  • 为什么旧结果会把当前页面改回去
  • 为什么页面离开后任务还在回写

这些问题表面看像 Taskasync/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 就能补好的。