WWDC 2025 deal: 30% off our Swift and SwiftUI books! Learn more ...WWDC 2025 deal:30% off our Swift and SwiftUI books >>

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.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

WWDC 2025 offer: 30% off!$35$25

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms
WWDC 2025 offer: 30% off!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35$25