I have the following code, that leads to an EXC_BAD_ACCESS and I cannot find the solution:
struct ContentView: View {
var body: some View {
VStack {
AsyncChangeButton() { reason in
print("reason is: \(reason)")
}
}
.padding()
}
}
struct AsyncChangeButton: View {
@State var reason = "test"
private var changeAction: (String) async -> Void
init(changeAction: @escaping (String) async -> Void) {
self.changeAction = changeAction
}
var body: some View {
VStack {
TextField("Reason", text: self.$reason)
Button("Change") {
self.callChangeAction()
}
}
}
private func callChangeAction() {
Task {
await self.changeAction(self.reason)
}
}
}
It crashes at the print-command. Hovering the reason in Xcode shows me a totally confusing Unicode.
Any ideas?
(Btw.: It is a simplified code example and yes, the closure must be async because I want to do some network stuff in the closure and when it's done, the AsyncChangeButton should do some additional work)
As it is still ambiguous what exactly might have happened at runtime, even with the example, can you provide the diagnostic you received?
I did create a example fiddle without SwiftUI but was unable to replicate a crash, is the closure signature different, like (inout String) async -> Void or do you perform something else that isn’t indicated by your example that may cause a race condition?
Side Note:
Try to remove as much SwiftUI from your examples as possible, if removing SwiftUI solves the issue it may be better placed on the Apple Developer Forums.
I’m not sure about the why right now, but if you remove the AsyncChangeButton initializer everything works for me. Could it be that you’re using Swift 5? Also check out this post: Approachable Concurrency
You're kidding?! It's working without the initializer! Unfortunately my real code needs an initializer (for example I have an optional @Binding with a default value (like .constant(true) in my case).
Yes, I am. I have a huge project and I am not able to switch yet to Swift 6.
Thanks for the link. It explains more to me now. I had the same code working in my app without any problems. Then I created a new project, because I wanted to make it multiplatform (before it was macOS only). Afther doing this, the crash started to occur. When I now switch approachable concurrency off, it works again.
when i tested your sample code, i found that if i explicitly annotated the closure as @MainActor like this:
private var changeAction: @MainActor (String) async -> Void
then it no longer crashed. alternatively, wrapping the callback in an explicit struct also solved the issue (possibly for a similar reason, since the implicit @MainActor-ness maybe got correctly propagated through).
i'm not certain, but my guess would be there is a bug (or bugs) when converting between the different function types involved in this code since there are various implicit transformations that may occur.
and if you enable the SIL verifier on the 6.2 compiler, it complains that there's an invalid function conversion due to a difference in the number of parameters when converting the parameter to the stored property in the initializer:
perhaps the Int/String param is actually the nonisolated(nonsending) actor parameter instead? yep looks like that's probably the issue – if you make the parameter an AnyObject, you'll get the MainActor printed out instead of the 'real' parameter: Compiler Explorer.
not sure if this bug has been reported or addressed yet, but definitely seems like one worth filing & fixing!
That's very interesting. Also, quite severe, in my opinion, given that this is the default behavior for new Xcode app projects currently. Do you want to file the issue if it hasn't been already?
As I told, it is only a simplified code. I do need async, because in the closure I do some networking code. The AsyncButton waits until the code is finished and then it closes the dialog window.
FWIW, this is trivially achievable without a single explicit Task or async:
Example
import SwiftUI
import Combine
class ViewModel: ObservableObject {
@Published var workInProgress = false
private let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
private let url = URL(string: "https://www.apple.com")!
func doSomething() {
workInProgress = true
session.dataTask(with: URLRequest(url: url)) { data, response, error in
MainActor.assumeIsolated {
self.workInProgress = false
}
}.resume()
}
}
struct ContentView: View {
@StateObject private var model = ViewModel()
var body: some View {
VStack {
Button("Do something") {
model.doSomething()
}
.sheet(isPresented: $model.workInProgress) {
Text("Work in progress")
}
}
.padding()
}
}
@main struct MyAppApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
Note that it is perfectly valid to initiate network request from the main thread and have it's completion delivered to the main thread (what's not advisable is performing a synchronous network I/O on the main thread (that blocks the thread) which is not happening here).