SwiftUI中.task与.refreshable刷新时请求被取消的问题
SwiftUI 刷新时出现 NSURLErrorDomain Code=-999 "cancelled" 问题解决
问题场景
使用SwiftUI开发时,通过.task modifier从远程拉取数据,后续用ScrollView.refreshable触发刷新操作,此时会抛出NSURLErrorDomain Code=-999 "cancelled"错误。即便改用模拟实现的PodcastProvider,该错误依然会复现,最终定位问题出在ProgressView的条件显示逻辑,但不清楚具体原因。
问题根源
当你通过if/else条件切换ProgressView和ScrollView的显示状态时,isLoading的变化会导致整个ScrollView被销毁并重新创建。而.refreshable是依附在ScrollView实例上的,当ScrollView被销毁时,系统会自动取消所有关联的未完成异步任务(包括刷新时的网络请求或模拟请求),从而触发-999错误。这个问题和实际网络请求无关,本质是视图层级变化导致的任务生命周期中断。
解决方案
不要通过条件替换整个ScrollView,而是用ZStack将ProgressView叠加在ScrollView上方,保持ScrollView的实例始终存在,避免任务被意外取消。
错误代码示例(触发问题)
struct PodcastListView: View { @State private var podcasts: [Podcast] = [] @State private var isLoading = true private let provider = PodcastProvider() var body: some View { // if/else切换会导致ScrollView被销毁重建 if isLoading { ProgressView("加载中...") } else { ScrollView { LazyVStack { ForEach(podcasts) { podcast in PodcastRow(podcast: podcast) } } } .refreshable { isLoading = true try? await provider.refreshPodcasts() podcasts = provider.currentPodcasts isLoading = false } .task { isLoading = true await provider.loadInitialPodcasts() podcasts = provider.currentPodcasts isLoading = false } } } }
修复后的代码示例
struct PodcastListView: View { @State private var podcasts: [Podcast] = [] @State private var isLoading = true private let provider = PodcastProvider() var body: some View { ZStack(alignment: .center) { ScrollView { LazyVStack { ForEach(podcasts) { podcast in PodcastRow(podcast: podcast) } } } .refreshable { isLoading = true try? await provider.refreshPodcasts() podcasts = provider.currentPodcasts isLoading = false } .task { isLoading = true await provider.loadInitialPodcasts() podcasts = provider.currentPodcasts isLoading = false } // ProgressView叠加在ScrollView上方,不破坏原有视图层级 if isLoading { ProgressView("加载中...") .background(Color.systemBackground.opacity(0.9)) .cornerRadius(8) .padding() } } } }
补充说明
- 即使使用模拟的
PodcastProvider也会复现错误,因为系统取消任务的逻辑和任务内容无关,只要异步任务绑定的视图实例被销毁,就会触发-999错误。 - 另一种可选方案是将ProgressView放在ScrollView内部的顶部(比如通过
if isLoading在LazyVStack最上方显示),同样可以避免ScrollView被销毁。
内容的提问来源于stack exchange,提问作者Valerio
相关产品推荐
相关产品推荐

