iOS16中ScrollView的refreshable修饰符为何会取消异步任务?
问题:iOS 16中ScrollView的refreshable修饰符导致异步任务被取消
我正在开发适配iOS 16的应用,尝试在ScrollView上使用refreshable修饰符,但下拉刷新手势过程中异步任务会被取消。
复现代码
ExploreViewModel.swift
class ExploreViewModel: ObservableObject { @Published var randomQuotes: [Quote] = [] init() { Task { await loadQuotes() } } @MainActor func loadQuotes() async { let quotesURL = URL(string: "https://type.fit/api/quotes")! do { let (data, urlResponse) = try await URLSession.shared.data(from: quotesURL) guard let response = urlResponse as? HTTPURLResponse else { print("no response"); return} if response.statusCode == 200 { let quotes = try JSONDecoder().decode([Quote].self, from: data) randomQuotes.append(contentsOf: quotes) } } catch { debugPrint(error) debugPrint(error.localizedDescription) } } func clearQuotes() { randomQuotes.removeAll() } }
ContentView.swift
import SwiftUI struct ContentView: View { @StateObject private var exploreVM = ExploreViewModel() var body: some View { NavigationStack { ExploreView() .environmentObject(exploreVM) .refreshable { exploreVM.clearQuotes() await exploreVM.loadQuotes() } } } }
ExploreView.swift
import SwiftUI struct ExploreView: View { @EnvironmentObject var exploreVM: ExploreViewModel var body: some View { ScrollView { VStack { LazyVGrid(columns: [GridItem(.adaptive(minimum: 140.0), spacing: 24.0)], spacing: 24.0) { ForEach(exploreVM.randomQuotes) { quote in VStack(alignment: .leading) { Text("\(quote.text ?? "No Text")") .font(.headline) Text("\(quote.author ?? "No Author")") .font(.caption) } .frame(minWidth: 0, maxWidth: .infinity) .frame(height: 144.0) .border(Color.red, width: 2.0) } } } .padding() .navigationTitle("Explore") } } }
问题演示与报错
下拉刷新手势过程中任务被取消的演示:
控制台报错截图:
解决方案
问题核心是refreshable修饰符的闭包任务会绑定到下拉手势生命周期,用户松开手指时系统会自动取消闭包内的异步任务,导致网络请求中断。以下是两种解决方式:
方式1:在ViewModel中维护独立刷新任务
修改ExploreViewModel,新增独立的刷新逻辑,脱离refreshable的取消上下文:
class ExploreViewModel: ObservableObject { @Published var randomQuotes: [Quote] = [] private var refreshTask: Task<Void, Error>? // 新增任务变量 init() { Task { await loadQuotes() } } @MainActor func loadQuotes() async { // 原加载逻辑不变 let quotesURL = URL(string: "https://type.fit/api/quotes")! do { let (data, urlResponse) = try await URLSession.shared.data(from: quotesURL) guard let response = urlResponse as? HTTPURLResponse else { print("no response"); return} if response.statusCode == 200 { let quotes = try JSONDecoder().decode([Quote].self, from: data) randomQuotes.append(contentsOf: quotes) } } catch { debugPrint(error) debugPrint(error.localizedDescription) } } func clearQuotes() { randomQuotes.removeAll() } // 新增独立刷新方法 func refreshQuotes() { refreshTask?.cancel() // 取消未完成的旧任务 refreshTask = Task { await MainActor.run { self.clearQuotes() } await loadQuotes() } } }
更新ContentView的refreshable闭包:
.refreshable { exploreVM.refreshQuotes() await exploreVM.refreshTask?.value // 可选:等待任务完成 }
方式2:在refreshable中使用Task.detached
无需修改ViewModel,直接在闭包内用Task.detached启动不受取消影响的任务:
.refreshable { await Task.detached { await MainActor.run { self.exploreVM.clearQuotes() } await self.exploreVM.loadQuotes() }.value }
内容的提问来源于stack exchange,提问作者Jobie J
相关产品推荐
相关产品推荐

