You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 08:05:33