Reputation: 677
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:
Upvotes: 3
Views: 1551
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
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
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