SwiftUI Tips: Handling the Esc Key
Recently, I tried to close my macOS app using the Esc
key. While there are plenty of tutorials explaining how to achieve this, I ran into a few challenges along the way. So, I thought I’d document my findings here.
NOTE: I’m not an expert in macOS development, so feel free to correct me if I’ve made any incorrect assumptions.
Basic Key Press Handling
Let’s start with how to listen for keyboard shortcuts in SwiftUI. There’s a modifier called onKeyPress
with several overloads. However, since I wanted to listen specifically for the Esc
key, I opted to use the onExitCommand
modifier instead. Why? Well, it’s easier to read, in my opinion.
.onExitCommand {
// Handle ESC key press
}
Making Views Respond to Keyboard Events
At this point, I assumed everything was set up to handle key presses. Not quite! To listen to any key press, a view needs to be focusable. That part is straightforward: just add the focusable
modifier before applying the key press listener. (Yes, the order matters!)
Now, I launched my app. It opened in the foreground, and I was ready to test. But when I pressed the Esc
key, my Mac played a “ding” sound, which meant the app’s view wasn’t focused. Strange, right? The app was interactive, yet the view didn’t have focus. I had to manually click the window or view to give it focus and see the focus ring appear. Only then did the key press listener work. Not cool.
I wanted my app’s window to automatically focus as soon as it appeared on the screen. That’s where @FocusState
came to the rescue. Here’s how I solved it:
- I created a simple
@FocusState
variable, like@FocusState private var isFocused: Bool
. - In the
onAppear
modifier, I set this variable totrue
. Similarly, I set it tofalse
inonDisappear
. - I connected the
@FocusState
variable to the view using thefocused($isFocused)
modifier.
With this setup, my app’s window automatically gained focus when it appeared on screen, and it correctly listened for the Esc
key press.
One last thing — I didn’t want the focus ring to appear in my app. It looked odd in this case. Thankfully, I could turn it off using the focusEffectDisabled()
modifier.
Here’s a snippet of my final code:
struct ContentView: View {
@FocusState private var isFocus: Bool
var body: some View {
MainContentView()
.focusable()
.focused($isFocus)
.onExitCommand {
NSApplication.shared.terminate(nil)
}
.onAppear {
isFocus = true
}
.onDisappear {
isFocus = false
}
}
}
Now, the app’s window gains focus automatically, listens for the Esc
key press, and doesn’t display the focus ring. Problem solved! 🎉