How to avoid SwiftUI warnings about "Publishing changes from within view updates is not allowed" when using onKeyPress and ObservableObject?

2.1k views Asked by At

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 answers

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.