RXP
RXP

Reputation: 677

Complete Concurrency check enabled and how to resolve warnings

I am looking to resolve the concurrency warnings that I am getting after enabling the complete concurrency check in Xcode.

Here is a simple example to illustrate the warning. Consider I have a ContentView that has a long running task on the background thread. When I call the long running task inside that of a Task {...} I get a "Passing argument of non-sendable type 'ContentView' outside of main actor-isolated context may introduce data races" warning. I get this warning when I enable the concurrency settings to complete in Xcode. I tried the suggestion to make the ContentView Sendable but it generates additional warnings(see second image below) and will not work in my situation. How do I get around this? appreciate any help/direction

See code below:

struct ContentView: View {
    @State private var disable: Bool = false
    @State private var value: Double = 0

    var body: some View {
        VStack {
            Button {
                disable = true
            } label: {
                Text("Press")
            }
            .task {
                await value = self.fetchValue() //<---Here is the warning. See image below
            }
            Text("\(value)")

        }
    }
}

extension ContentView {
    
    func fetchValue() async -> Double {
        //long running task
        try? await Task.sleep(nanoseconds: 60)
        return 2.0
    }
}

Here is the warning:

enter image description here

enter image description here

Upvotes: 3

Views: 1551

Answers (3)

iosDEVSwift
iosDEVSwift

Reputation: 1

struct ContentView: View {
    @State private var disable: Bool = false
    @State private var value: Double = 0

    var body: some View {
        VStack {
            Button(action: {
                disable = true
                fetchAndUpdateValue()
            }) {
                Text("Press")
            }
            Text("\(value)")
        }
    }
    
    private func fetchAndUpdateValue() {
        Task {
            let fetchedValue = await fetchValue()
            value = fetchedValue
        }
    }
}

extension ContentView {
    private func fetchValue() async -> Double {
        // Simulate long running task
        await Task.sleep(nanoseconds: 60)
        return 2.0
    }
}

I've created a new private function fetchAndUpdateValue() that performs the async operation and updates the value property accordingly.

When the button is pressed, it sets the disable state and invokes fetchAndUpdateValue() to fetch the value asynchronously.

The fetchValue() function remains unchanged, still performing the async operation.

Upvotes: 0

lorem ipsum
lorem ipsum

Reputation: 29291

A SwiftUI View is not attached to an actor the body is decorated with @MainActor but not the View itself.

When you have func in the View you can decorate the View with @MainActor the same way you would a "ViewModel".

import SwiftUI
@MainActor // Decorate the View
struct AsyncTest: View {
    @State private var isRunning: Bool = false
    @State private var value: Double = 0

    var body: some View {
        VStack {
            Button {
                print("press")
                isRunning = true
            } label: {
                Text("Press")
            }.disabled(isRunning)
                .task(id: isRunning) {
                    guard isRunning else {return}
                    print("start")
                    value = await fetchValue()
                    print("end")
                    isRunning = false
                }
            if isRunning {
                ProgressView()
            }
            
            Text("\(value)")

        }
    }
}

extension AsyncTest {
    
    func fetchValue() async -> Double {
        await Task.detached(priority: .userInitiated) {
            // Mock long runing task
            _ = (0...10000000).map { n in
                n.description
            }.sorted(using: KeyPathComparator(\.description))
            //long running task
            //try? await Task.sleep(nanoseconds: 60)
            return 2.0
        }.value
    }
}
#Preview {
    AsyncTest()
}

Upvotes: 4

malhal
malhal

Reputation: 30575

It's because you have the order of await and value muddled up, try this:

.task {
    value = await AsyncController().fetchValue()
}

Also it's best to change extension AsyncTest to struct AsyncController or something, that way it is not run on the main thread which is what happens to any async func declared inside the View once you use @StateObject or @ObservedObject. What matters is where the func is declared.

struct AsyncController {
    
    func fetchValue() async -> Double {
        //long running task
        try? await Task.sleep(nanoseconds: 60)
  
        // safe to do processing here because this will be a background thread

        return 2.0
    }
}

Even better, make the AsyncController an EnvironmentKey so you can mock it for Previews via .environment.

Upvotes: -3

Related Questions