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

pdtvr36n  于 2023-01-03  发布在  iOS
关注(0)|答案(2)|浏览(128)

我试图在一个面向iOS 16的应用程序中的滚动视图上使用可刷新修改器。但是,在拉取以刷新手势期间,异步任务被取消。
下面是演示该问题的一些代码和随附的视频以及打印错误的图像:
ExploreViemModel.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()
                }
        }
    }
}

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

fhg3lkii

fhg3lkii1#

当调用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)
    }
}

如果使用上述方法,则应删除ExploreViewModelinit中的"浮动" Task

    • 备选案文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可能是苹果设想的过程,但也容易受到其他重画。

4c8rllxm

4c8rllxm2#

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

            // usually we store the error in another state.
        }
    }
}

相关问题