i2h3
1
The use case is pretty simple. I have a SwiftUI view from which I dispatch a task for asynchronous background work during the click on a button. When an error is thrown, I want to catch it in the task and propagate the error message back to the SwiftUI view.
Steps To Reproduce
- Create a new SwiftUI app project for macOS in Xcode.
- Replace the template
ContentView with the code below.
- Set "Strict Concurrency Checking" to "Complete" in the build settings of the project.
ContentView.swift
struct ContentView: View {
@State private var errorMessage: String?
var body: some View {
VStack {
Button(action: doSomething) {
Text("Do Something")
}
}
.padding()
}
func doSomething() {
Task {
errorMessage = "Oops!"
}
}
}
Xcode reports a warning for the line in which I assign a value to errorMessage:
Capture of 'self' with non-sendable type 'ContentView' in a @Sendable closure
I understand that. But how can I resolve that problem? How can I dispatch a code block back in the original concurrency domain of the SwiftUI view here while retaining the new concurrency patterns? Obviously, the view itself is not supposed to be @Sendable. On the other hand, I cannot reference a single thing in all of my ideas without encountering this warning.
tera
2
You could mark your view with "@MainActor" to get rid of that warning.
If you do that you'll get another warning in a different place:
Button(action: doSomething) {
// 🔶 Warning: Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor 'MainActor'
That warning you could sweep under the carpet:
extension Button {
init(_ action: @MainActor @escaping () -> Void, @ViewBuilder label: () -> Label) {
self.init(action: action, label: label)
// 🔶 Warning: Converting function value of type '@MainActor () -> Void' to '() -> Void' loses global actor 'MainActor'
}
}
Button(doSomething) { // ✅
Text("Do Something")
}
I'm not sure this is the best way.
i2h3
3
This made me think and apparently I figured it out. Instead of passing the function directly, I introduce a closure with a call. Not very elegant but free of issues. 
@MainActor
struct ContentView: View {
@State private var errorMessage: String?
var body: some View {
VStack {
Button {
doSomething()
} label: {
Text("Do Something")
}
Text("Error message: \(errorMessage ?? "")")
}
.padding()
}
func doSomething() {
Task {
errorMessage = "Oops!"
}
}
}
Yeah, not terrible, but it does seem like boilerplate you shouldn't have to do. And I suspect it's merely taking advantage of a gap (i.e. a bug) in the checks, since calling doSomething from your closure logically makes your closure @MainActor too.
SwiftUI is unfortunately full of these Strict Concurrency Checking problems - I think because SwiftUI simply predates things like @MainActor, otherwise it would have used them from the start. I wonder if adding @MainActor is ABI-compatible? It's not source-compatible, strictly speaking, but then neither are the Strict checks (which are intended to become on by default in Swift 6; source breakage is coming… I'm not sure what the library migration story is there, if the necessary changes are binary-incompatible…).
1 Like
tera
5
Perhaps. FWIW there's an explicit marker we can put on closures to make them @MainActor:
{ @MainActor in mainActorOnlyCall() }
If we do it in this example the warning is back.
Button { @MainActor in
doSomething() // Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor 'MainActor'
} label: {
Text("Do Something")
}
What should happen in the following example? Should it be treated as an error?
{
mainActorOnlyCall()
someOtherActorOnlyCall()
}
Seems like…? Unless I'm missing something, it seems like a clear case of erroneously conflating isolation domains.
1 Like