Nov 29, 2022 • SwiftUI, Swift Concurrency
Calling Mutating Async Functions from SwiftUI Views
Whenever we try to make a call from a SwiftUI view to a mutating asynchronous function on a struct, we are greeted with an error message similar to this one:
Cannot call mutating async function 'someFunction()' on actor-isolated property 'someStruct'.
Let's assume we have the following data structure design which allows us to unlock an app with a three second delay:
import Combine
@MainActor
class AppData: ObservableObject {
@Published var status: AppStatus = AppStatus()
}
struct AppStatus {
private(set) var isLocked = true
mutating func unlock() async
{
try? await Task.sleep(nanoseconds: 3_000_000_000)
isLocked = false
}
}
Now suppose we want to call the async mutating function unlock()
from a SwiftUI view:
import SwiftUI
struct ContentView: View {
@StateObject var appData = AppData()
@State var unlocking = false
var body: some View {
VStack {
if appData.status.isLocked {
Text("Content is locked.")
Button("Tap here to unlock") { Task {
unlocking = true
await appData.status.unlock() // Error: Cannot call mutating async function 'unlock()' on actor-isolated property 'appData'.
} }
.disabled(unlocking)
} else {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
.padding()
}
}
As it is, we'll get the familiar error: Cannot call mutating async function 'unlock()' on actor-isolated property 'appData'.
To work around this issue we can simply add a proxy function to our struct
's Binding
type which will in turn call the original mutating function. It's important that we mark this extension @MainActor
to avoid the infamous purple error reminding us that publishing changes from background threads is not allowed.
import SwiftUI
@MainActor
extension Binding where Value == AppStatus {
func unlock() async {
await wrappedValue.unlock()
}
}
Now we can simply modify the original call site and add a $
sign to appData
to access the binding and call our proxy.
…
Button("Tap here to unlock") { Task {
unlocking = true
await $appData.status.unlock() // No longer gives error.
} }
…
This technique allows us to continue to leverage value types and incorporate even more sophisticated logic into our models.