swiftmacosswiftuiappkit

Is there a way to determine if any text input was active in a MacOS app after the app was backgrounded?


MacOS specific but maybe iOS related responses could get me on the right track as well:

I need to determine wether there was any text input (swiftUI text field, NSTextField) active app wide when the app lost focus. This means the user either minimised the window of my app or clicked on the window of a different app. Is there a NSApplication (or AppKit in general) API i could use to achieve this? Under the hood something definitely exists because the field that was active when the app was last in focus becomes active again when the app regains focus. Unfortunately i think whatever api is used to achieve that was not made public.

Here is a code example of my needs:

struct FooView: View {
    @Environment(\.controlActiveState) private var controlActiveState

    var body: some View {
        Text("Hello World")
            .onChange(of: controlActiveState) { newState in
                guard !(newState == .inactive) else { return }
                let wasAnyTextInputActive: Bool = false // <- need to determine if any textfield was active app wide here
                guard wasAnyTextInputActive else { return }
                print("Should only be reached when the app was backgrounded with an active text input")
            }
    }
}

Thanks!


Solution

  • The NSWindow in your application that receives keyboard events is called the “key window”, and you can find it in the NSApplication keyWindow property.

    When an application is not active, AppKit sets keyWindow to nil. When the application becomes active again, AppKit restores keyWindow.

    In AppKit (and UIKit), keyboard events are routed along a structure called the “responder chain”. The first responder in the chain is called the “first responder”. You can find a window's current first responder in its firstResponder property. (Note that UIWindow does not expose a firstResponder property, although it has one internally.)

    So, you can check, when your app is becoming inactive, whether it has a key window and (if so) ask what the first responder in that key window is. However, controlActiveState is updated after AppKit sets keyWindow to nil, so you need to subscribe to NSApplication.willResignActiveNotification instead.

    In the Keyboard system settings, there is a “Keyboard navigation” option that allows non-text-entry controls (like buttons) to become first responder. This allows the user to manipulate more of the user interface entirely from the keyboard. Because of this, you may also want to check that the first responder is an NSTextView. Note that in AppKit, an NSTextField cannot normally be the first responder. When you try to focus an NSTextField, it secretly gives itself an NSTextView subview called the “field editor”, and the field editor becomes the first responder. Thus if you want to know whether any text entry control in your app has the keyboard focus, it usually suffices to check whether your app has a keyWindow whose firstResponder is an NSTextView.

    This example should get you started:

    struct ContentView: View {
        @State var text = "hello"
        @FocusState var fieldIsFocused
    
        var body: some View {
            VStack {
                TextField("Text", text: $text)
                    .focused($fieldIsFocused)
    
                Text(fieldIsFocused ? "Field is focused" : "Field is not focused")
    
                Button("Unfocus") {
                    fieldIsFocused = false
                }
            }
            .padding()
            .onReceive(
                NotificationCenter.default
                    .publisher(for: NSApplication.willResignActiveNotification)
            ) { _ in
                print("key window: \(NSApplication.shared.keyWindow)")
                print("key window's first responder: \(NSApplication.shared.keyWindow?.firstResponder)")
                print("key window's first responder is NSTextView: \(NSApplication.shared.keyWindow?.firstResponder is NSTextView)")
            }
        }
    }