eli_slade
eli_slade

Reputation: 509

Is there a way to have dynamic commands using SwiftUI commands without fallback to AppKit

So far I've tried watching the published value of the App events and updating the @State property wrapper, but alas this has no runtime effect on the MenuBar commands.

struct MyCommands: Commands {
    
    @State private var isShift = false
    
    var body: some Commands {
        CommandMenu("Things"){
            Button("Thing A\(isShift ? " Shift" : "")"){
                if isShift {
                    print("Thing A Shift")
                } else {
                    print("Thing A")
                }
            }
            .keyboardShortcut(.init(isShift ? "S" : "s"))
            .onReceive(NSApplication.shared.publisher(for: \.currentEvent)) {
                if let evt = $0 {
                    isShift = evt.modifierFlags.contains(.shift)
                }
            }
        }
    }
    
}

Upvotes: 1

Views: 436

Answers (2)

eli_slade
eli_slade

Reputation: 509

I got it to work by actually taking the advice of ChrisR and moving the event detection to a StateObject and passing value into commands. For some reason the view diff won't change if updating commands from inside itself. Though not entirely sure if this is a bug or a feature.

Note: this is also visually jarring as you can see the menu flash for second when pressing shift, but in AppKit the menu would update instantly without flashing.

class EventWatch: ObservableObject {
    
    @Published private(set) var modifiers: EventModifiers = []
    
    private var watch: Set<AnyCancellable> = []
    
    init(){
        NSApplication.shared.publisher(for: \.currentEvent).sink{
            if let event = $0 {
                if event.type == .flagsChanged {
                    self.modifiers = EventModifiers(rawValue: Int(event.modifierFlags.rawValue))
                }
            }
        }.store(in: &watch)
    }
    
}
@main
struct MyApp: App {
    
    @StateObject private var evt = EventWatch()
    
    var body: some Scene {
        WindowGroup("MyGroup") {
            Text("My View").frame(minWidth: 1000, minHeight: 600)
        }
        .commands { MyCommands(isShift: evt.modifiers.contains(.shift)) }

    }
}

Upvotes: 0

ChrisR
ChrisR

Reputation: 12165

This code just toggles the menu command by selecting it, and this works. So the question remains why the .onReceivedoesnt do it ...? Even if the @State change wouldn't trigger an immediate redraw in commands, it should still influence the next redraw on selecting the menu again.

struct MyCommands: Commands {
    
    @State private var isShift = false
    
    var body: some Commands {
        CommandMenu("Things"){
            Button("Thing A\(isShift ? " Shift" : "")"){
                if isShift {
                    print("Thing A Shift")
                } else {
                    print("Thing A")
                }
                isShift.toggle()
            }
        }
    }
}

Upvotes: 1

Related Questions