为什么在 ScrollView 上的可刷新修饰符中取消异步任务(iOS 16)

问题描述 投票:0回答:5

我正在尝试在面向 iOS 16 的应用程序中的 Scrollview 上使用可刷新修饰符。但是,异步任务在拉动刷新手势期间被取消。

这里有一些代码和附加的视频,演示了问题以及带有打印错误的图像:

探索ViemModel.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()
                }
        }
    }
}

探索.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")
        }
 
    }
}

ios swiftui scrollview
5个回答
9
投票

当您调用

exploreVM.clearQuotes()
时,您会导致
body
在数组被清除时重绘。

.refreshable
也会被重绘,因此之前正在使用的“任务”被取消。

这就是 SwiftUI 的本质。

有几种方法可以克服这个问题,最简单的方法是使用

id
“坚持”任务。

选项1

struct ExploreParentView: View {
    @StateObject private var exploreVM = ExploreViewModel()
    //@State can survive reloads on the `View`
    @State private var taskId: UUID = .init()
    var body: some View {
        NavigationStack {
            ExploreView()
                .refreshable {
                    print("refreshable")
                    //Cause .task to re-run by changing the id.
                    taskId = .init()
                }
            //Runs when the view is first loaded and when the id changes.
            //Task is perserved while the id is preserved.
                .task(id: taskId) {
                    print("task \(taskId)")
                    exploreVM.clearQuotes()
                    await exploreVM.loadQuotes()
                }
        }.environmentObject(exploreVM)
    }
}

如果您使用上述方法,您应该删除

Task
init
中的“浮动”
ExploreViewModel

选项2

另一种方法是防止重新绘制,直到 url 调用返回。

class ExploreViewModel: ObservableObject {
    //Remove @Published
    var randomQuotes: [Quote] = []
    
    init() {
        //Floading Task that isn't needed for option 1
        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)
                print("updated")
            }
        } catch {
            debugPrint(error)
            debugPrint(error.localizedDescription)
        }
        print("done")
        //Tell the View to redraw
        objectWillChange.send()
    }
    
    func clearQuotes() {
        randomQuotes.removeAll()
    }
}

选项3

就是等待改变数组,直到有响应。

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)
                //Replace array
                randomQuotes = quotes
                print("updated")
            }
        } catch {
            //Clear array
            clearQuotes()
            debugPrint(error)
            debugPrint(error.localizedDescription)
        }
        print("done")
    }
    
    func clearQuotes() {
        randomQuotes.removeAll()
    }
}

选项 1 更能抵抗取消,对于短线通话来说是可以的。它不会等待调用返回来关闭 ProgressView。

选项 2 在 ViewModel 中提供更多控制,但视图仍然可以由其他人重绘。

选项 3 很可能是 Apple 设想的流程,但也容易受到其他重绘的影响。


4
投票

我也刚刚遇到了这个问题,我不确定为什么

.refreshable
的行为是这样的。 我的解决方案是用一个新任务将逻辑包装在
.refreshable {}
中,然后等待结果。

.refreshable {
    await Task {
        exploreVM.clearQuotes()
        await exploreVM.loadQuotes()
    }.value
}

这样,即使可刷新任务被取消,也无法取消新任务,因此新任务将运行直到完成。

这样做的一个好处是刷新旋转器应该显示直到新任务完成。


0
投票

您可以尝试将方法包装到非结构化任务,如下所示:

import SwiftUI

struct ContentView: View {
    
    @StateObject private var exploreVM = ExploreViewModel()
    
    var body: some View {
        
        NavigationStack {
            ExploreView()
                .environmentObject(exploreVM)
                .refreshable {
                    exploreVM.clearQuotes()
                    // Lifecycle task
                    Task {
                        await exploreVM.loadQuotes()
                    }
                }
        }
    }
}

0
投票

`

.refreshable {
   Task {
       await model.refresh()
   }
}

`


-1
投票

async/await 和

.task
的要点是消除对引用类型的需要。试试这个:

struct ContentView: View {
    
    @State var randomQuotes: [Quote] = []
    
    var body: some View {
        NavigationStack {
            ExploreView()
                .refreshable {
                    await loadQuotes()
                }
        }
    }

      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 {
                randomQuotes = try JSONDecoder().decode([Quote].self, from: data)
            }
        } catch {
            debugPrint(error)
            debugPrint(error.localizedDescription)

            // usually we store the error in another state.
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.