我正在尝试在面向 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")
}
}
}
当您调用
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 设想的流程,但也容易受到其他重绘的影响。
我也刚刚遇到了这个问题,我不确定为什么
.refreshable
的行为是这样的。
我的解决方案是用一个新任务将逻辑包装在 .refreshable {}
中,然后等待结果。
.refreshable {
await Task {
exploreVM.clearQuotes()
await exploreVM.loadQuotes()
}.value
}
这样,即使可刷新任务被取消,也无法取消新任务,因此新任务将运行直到完成。
这样做的一个好处是刷新旋转器应该显示直到新任务完成。
您可以尝试将方法包装到非结构化任务,如下所示:
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()
}
}
}
}
}
`
.refreshable {
Task {
await model.refresh()
}
}
`
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.
}
}
}