简体   繁体   中英

VStack messes with task modifier?

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

Or a GIF for convenience: 在此处输入图像描述

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()
    }
}

Complete findings and code is here

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM