I'm creating a view called AsyncDataView
. The idea is, whenever the view appears, it will start an asynchronous Task
to download data from wherever. While its in the process of downloading, the view should display a ProgressView
.
Once the Task
is complete and the data has been downloaded, the view should display the downloaded data in a Text
view.
I thought this would be a fairly easy implementation, here's a minimal example:
struct ContentView: View {
let items = 0..<100
var body: some View {
NavigationView {
List(items, id: \.self) { _ in
// Removing the VStack resolves the issue!
VStack {
AsyncDataView()
}
}
Text("Middle Panel")
}
}
}
struct AsyncDataView: View {
@State private var data: String?
@ViewBuilder private var mainBody: some View {
if let data = data {
Text(data)
}
else {
ProgressView()
}
}
var body: some View {
mainBody
.task {
// Simulate loading data from a website or whatever
try? await Task.sleep(nanoseconds: 1_000_000_000)
// @State is thread safe according to Apple docs
data = String(Int.random(in: 0..<100))
}
}
}
Here, I just have a List
of 100 of these AsyncDataView
's. Whenever an AsyncDataView
appears, a Task
is started. The Task
just sleeps 1 second to simulate downloading, and then assigns data.
Problem : Whenever I scroll down the List
really quickly, and then scroll back up, some items never finish loading. They just remain with the spinning ProgressView
.
It's best displayed with a video: https://imgur.com/a/ERH8477
In the video, I wait for the initial items to be loaded. I then scroll down, and then back up. At the end, you can see 4 items which never loaded.
The kicker : The weirdest thing is that if I remove the VStack
inside the body of the List
then this issue resolves! It appears the VStack
messes with the task
modifier, Or, what I've also seen, is maybe the VStack
resets the @State
inside of the AsyncDataView
for some reason?
I would just remove the currently redundant VStack
, but the issue is in my actual use-case I need to have another view displayed below this one.
Is anybody able to reproduce this, and does anybody know what's up with this code? Or is this a SwiftUI bug, as @Asperi suggested?
This was done on macOS Monterey 12.3.1 (21E258)
.
The solution appears to be platform dependent.
A possible approach is to move async fetching data logic into view model and leave view update just on main actor.
Tested with Xcode 13.3 / macOS 12.2
Main part:
class ViewModel: ObservableObject {
@Published var data: String?
init() {
update()
}
func update() {
Task.detached {
try? await Task.sleep(nanoseconds: 1_000_000_000)
await MainActor.run {
self.data = String(Int.random(in: 0..<100))
}
}
}
}
// ...
@StateObject private var vm = ViewModel()
var body: some View {
if let data = vm.data {
Text(data)
}
else {
ProgressView()
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.