swiftuiios17property-wrapper-publishedmacos-sonoma

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


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()
}

Solution

  • 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