Reading keyboard modifiers on iPad in SwiftUI
With the advent of desktop class iPadOS applications there are many macOS UX idioms I would like to bring to our iPad apps. One such UX pattern is hybrid mouse + keyboard interactions, such as command + click for adding or removing items from selection and shift + click for extending the selection. The standard List component does provide these modifier driven selection actions by default if we use the init(selection:content:) constructor. Unfortunately, there is no equivalent for grid or other custom layouts. So I needed to find a way to detect which keys are being pressed as the user takes an action.
In SwiftUI on macOS we have the ability to add modifiers(_:) to Gestures but the modifiers(_:)
API is not available on iPad. For iPadOS I have found that the Game Controller framework offers a solution.
# Get a reference to the keyboard
To check if shift
or command
are pressed I must first find the current attached keyboard, if any. GCKeyboard offers a static property coalesced, but it can sometimes return nil
when read within a button or gesture block, even when a keyboard is attached.
The GCKeyboardDidConnect notification provides another way to get the current keyboard. The notification will fire imminently on subscription if a keyboard is attached and the object
of the notification is the attached GCKeyboard
.
To ensure my entire app can read the keyboard's state I have created a top level ObservableObject
that acts as the GCKeyboardDidConnect
notification subscriber.
import GameController
class KeyboardObserver: ObservableObject {
@Published var keyboard: GCKeyboard?
var observer: Any? = nil
init() {
observer = NotificationCenter.default.addObserver(
forName: .GCKeyboardDidConnect,
object: nil,
queue: .main
) { [weak self] notification in
self?.keyboard = notification.object as? GCKeyboard
}
}
}
As there can only be one active keyboard, it makes sense for the observer to be an application wide StateObject
providing the same keyboard to all scenes. I pass it down the hierarchy as an environment object, so that all the views in my app have access to the keyboard.
@main
struct MyApp: App {
@StateObject var keyboardObserver = KeyboardObserver()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(keyboardObserver)
}
}
}
# Modify button behaviour
In my views I can now alter the behaviour of buttons and gestures by reading the current state of the GCKeyboardInput.
struct ContentView: View {
@EnvironmentObject var keyboardObserver: KeyboardObserver
var body: some View {
Button("Tap Me") {
guard let keyboardInput = keyboardObserver
.keyboard?.keyboardInput
else {
// default action, no keyboard found
return
}
let commandIsPressed = keyboardInput.button(
forKeyCode: .leftGUI
)?.isPressed ?? false || keyboardInput.button(
forKeyCode: .rightGUI
)?.isPressed ?? false
let shiftIsPressed = keyboardInput.button(
forKeyCode: .leftShift
)?.isPressed ?? false || keyboardInput.button(
forKeyCode: .rightShift
)?.isPressed ?? false
if commandIsPressed {
// action with CMD pressed
} else if shiftIsPressed {
// action with SHIFT pressed
} else {
// default action
}
}
}
}
When checking for pressed keys I found that it's important to check both the left and right versions of each GCKeyCode. GCKeyboardInput
also offers the ability to register a keyChangedHandler that I have not used yet. I expect to register the keyChangedHandler
so that I can expose additional advanced UI when the option
key is pressed, just like macOS.


Check out our book!
Integrating SwiftUI into UIKit Apps
Integrating SwiftUI intoUIKit Apps
UPDATED FOR iOS 17!
A detailed guide on gradually adopting SwiftUI in UIKit projects.
- Discover various ways to add SwiftUI views to existing UIKit projects
- Use Xcode previews when designing and building UI
- Update your UIKit apps with new features such as Swift Charts and Lock Screen widgets
- Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit