I've been trying to use the new onKeyPress functionality in SwiftUI 5. However, updates to a @Published property of an Observable Object within the handler produce a warning "Publishing changes from within view updates is not allowed, this will cause undefined behavior."

Note I haven't seen anything actually go wrong in practice, but Xcode logs a lot of red warnings in Console and purple runtime issues.

A simple replication; can modify with button or modify local @State without the warning; however modifying the @Published property from onKeyPress produces the warning.

import SwiftUI

@MainActor
class ExampleService: ObservableObject {
  @Published var text = ""
}

struct ContentView: View {
  @StateObject var service = ExampleService()
  
  @State var localText = ""
  
    var body: some View {
      VStack {

        Button {
          // This is fine
          service.text += "!"
        } label: {
          Text("Press Me")
        }
        
        Label("Example Focusable", systemImage: "arrow.right")
          .focusable()
          .onKeyPress { action in
            // XCode whines about "Publishing changes from within view updates is not allowed, this will cause undefined behavior."
            service.text += "."
            return .handled
          }
        
        Label("Example Local State", systemImage: "arrow.left")
          .focusable()
          .onKeyPress { action in
            // This is fine
            localText += "."
            return .handled
          }
        
        Text(service.text)
        Text(localText)
      }
    }
}

#Preview {
    ContentView()
}
2

There are 2 best solutions below

0
workingdog support Ukraine On BEST ANSWER

To make the warning disappear, use

DispatchQueue.main.async {  
    service.text += "." 
}

that will ensure the UI update is carried out on the main thread, as required by SwiftUI. Note, as mentioned in the comments you can also use Task{...}.

As I understand it, @MainActor is supposed to make execution on the main queue, but (I guess) not always. See this proposal to remove it from Swift 6: https://github.com/apple/swift-evolution/blob/main/proposals/0401-remove-property-wrapper-isolation.md

0
James On

An alternative approach (and probably what Apple want us to do now) is using @Observable

// No warnings in Xcode
@Observable class CompletionState {
  var text = ""
}

// Purple warnings in Xcode
class CompletionState2: ObservableObject {
  @Published var text = ""
}

It seems given Observable also offers more fine grained reactivity it is basically the preferred approach anyway. I've found updating an existing app was surprisingly straightforward. Apple have a guide on doing this here.