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!
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)")
}
}
}