Search code examples
macosswiftuicombineappkitnsnotifications

How to observe for modifier key pressed (e.g. option, shift) with NSNotification in SwiftUI macOS project?


I want to have a Bool property, that represents that option key is pressed @Publised var isOptionPressed = false. I would use it for changing SwiftUI View.

For that, I think, that I should use Combine to observe for key pressure.

I tried to find an NSNotification for that event, but it seems to me that there are no any NSNotification, that could be useful to me.


Solution

  • Since you are working through SwiftUI, I would recommend taking things just a step beyond watching a Publisher and put the state of the modifier flags in the SwiftUI Environment. It is my opinion that it will fit in nicely with SwiftUI's declarative syntax.

    I had another implementation of this, but took the solution you found and adapted it.

    
    import Cocoa
    import SwiftUI
    import Combine
    
    struct KeyModifierFlags: EnvironmentKey {
        static let defaultValue = NSEvent.ModifierFlags([])
    }
    
    extension EnvironmentValues {
        var keyModifierFlags: NSEvent.ModifierFlags {
            get { self[KeyModifierFlags.self] }
            set { self[KeyModifierFlags.self] = newValue }
        }
    }
    
    struct ModifierFlagEnvironment<Content>: View where Content:View {
        @StateObject var flagState = ModifierFlags()
        let content: Content;
    
        init(@ViewBuilder content: () -> Content) {
            self.content = content();
        }
    
        var body: some View {
            content
                .environment(\.keyModifierFlags, flagState.modifierFlags)
        }
    }
    
    final class ModifierFlags: ObservableObject {
        @Published var modifierFlags = NSEvent.ModifierFlags([])
    
        init() {
            NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
                self?.modifierFlags = event.modifierFlags
                return event;
            }
        }
    }
    

    Note that my event closure is returning the event passed in. If you return nil you will prevent the event from going farther and someone else in the system may want to see it.

    The struct KeyModifierFlags sets up a new item to be added to the view Environment. The extension to EnvironmentValues lets us store and retrieve the current flags from the environment.

    Finally there is the ModifierFlagEnvironment view. It has no content of its own - that is passed to the initializer in an @ViewBuilder function. What it does do is provide the StateObject that contains the state monitor, and it passes it's current value for the modifier flags into the Environment of the content.

    To use the ModifierFlagEnvironment you wrap a top-level view in your hierarchy with it. In a simple Cocoa app built from the default Xcode template, I changed the application SwiftUI content to be:

    struct KeyWatcherApp: App {
        var body: some Scene {
            WindowGroup {
                ModifierFlagEnvironment {
                    ContentView()
                }
            }
        }
    }
    

    So all of the views in the application could watch the flags.

    Then to make use of it you could do:

    struct ContentView: View {
        @Environment(\.keyModifierFlags) var modifierFlags: NSEvent.ModifierFlags
    
        var body: some View {
            VStack {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                if(modifierFlags.contains(.option)) {
                    Text("Option is pressed")
                } else {
                    Text("Option is up")
                }
            }
            .padding()
        }
    }
    

    Here the content view watches the environment for the flags and the view makes decisions on what to show using the current modifiers.