SwiftUI初始网络请求URLSession抛出cancelled错误排查
错误产生原因
- 首次启动抛出
cancelled错误的核心原因是SwiftUI.task修饰符的生命周期与TabView首次加载的渲染机制存在冲突:- 应用冷启动阶段,TabView会先对首个标签页
HomeView做一次初始渲染,此时.task修饰符自动创建异步任务发起网络请求 - 随后TabView完成自身根视图布局校准、NavigationView初始化等系统级视图层级调整,首次创建的
HomeView挂载的异步任务会被系统主动取消,抛出URLError.cancelled(错误码-999) - 视图层级稳定后,
.task会自动创建新的异步任务重新发起请求,这也是错误弹窗闪一下自动消失、后续所有请求都正常运行的根本原因
- 应用冷启动阶段,TabView会先对首个标签页
- 之前把请求放在根视图
ContentView时没有出现该问题,是因为根视图不会在启动阶段被系统重建/重新挂载,对应.task的任务不会被取消,但代价是根视图下所有子视图(包含TabBar、所有标签页)都会跟随状态变化整体刷新,存在不必要的性能损耗。
修复方案
方案1:错误捕获逻辑过滤取消错误(推荐,改动最小)
Swift并发体系下,任务因视图生命周期变化被取消属于正常行为,不属于需要提示用户的业务错误,只需要在ViewModel层识别这类取消错误,不触发错误弹窗即可。
修改ResultViewModel中的getResult方法:
func getResult() async { self.state = .loading self.hasError = false do { let results = try await service.fetchResults() // 校验任务是否在请求过程中被取消 try Task.checkCancellation() self.state = .success(data: results) } catch is CancellationError, let urlError = error as? URLError, urlError.code == .cancelled { // 捕获到任务取消类错误直接返回,不触发错误提示 return } catch { // 仅真实业务错误触发错误弹窗 self.state = .failed(error: error) self.hasError = true } }
该方案完全符合SwiftUI的设计逻辑,不需要调整视图层级,也不会引入额外的重复请求问题。
方案2:调整请求触发时机,避开视图首次挂载的波动阶段
如果不希望在错误逻辑中增加判断,可以将首次请求的触发时机从.task移到视图渲染完成后的下一个主线程周期,等视图层级完全稳定后再发起请求,避免任务被系统取消。
将HomeView中绑定的.task修饰符替换为以下代码:
.onAppear { // 跳过当前渲染周期,等视图层级稳定后再触发请求 DispatchQueue.main.async { Task { await viewModel.getResult() } } }
注意:该方案存在小缺陷,后续从其他页面返回HomeView触发onAppear时会重复发起请求,需要额外增加状态标识判断是否已经完成过首次加载。
额外优化建议
- 当前网络层代码使用
try?解码数据会吞掉具体的解码错误,不利于排查问题,建议将解码逻辑改为let decodedResponse = try JSONDecoder().decode(Result.self, from: data),直接抛出真实的解码错误信息。 - 如果后续TabView新增更多标签页,可将
ResultViewModel的初始化提升到TabView层级,通过环境Object注入给子视图,避免子视图重复初始化ViewModel导致的重复请求问题。
内容的提问来源于stack exchange,提问作者Andres Marquez
相关产品推荐
相关产品推荐

