Keyboard-driven actions in SwiftUI with onKeyPress

Keyboard-driven actions in SwiftUI with onKeyPress

Learn how to capture and respond to the pressed keys in a hardware keyboard in a SwiftUI app.

In iOS 17 and later, you can use the onKeyPress(_:action:) method to make a view respond to a physical keyboard event. It detects when a key is pressed and lets you perform actions based on it.

To take advantage of this method, be sure the view is focusable.

struct ContentView: View {
    
    @FocusState var isFocused: Bool
    
    var body: some View {
            
        MyView()
            .focusable()
            .focused($isFocused)
    }
}

Then, apply the modifier to your view.

MyView()
    .focusable()
    .focused($isFocused)
    
    // 1.
    .onKeyPress { pressedKey in
        // 2.
        print("You have just pressed \(pressedKey.key)")
        // 3.
        return .handled
    }

The method works as follows:

  1. It receives a parameter of type KeyPress, a type describing the key that was just pressed by the user.
  2. It performs an action once one or more keys have been pressed.
  3. Then, it returns a KeyPress.Result enum value specifying whether the action handled the event or not. Use:
    1. handled when the action successfully processed the event, stopping any further dispatch;
    2. ignored when the action did not process the event, allowing dispatch to proceed.
The “Failed to get or decode unavailable reasons” error is triggered when tapping a key on a non-physical keyboard. Be sure to allow the use of the modifier when a hardware keyboard is available.

Here is an example of how to fully implement the modifier in a SwiftUI view.

import SwiftUI

struct ContentView: View {
    @FocusState var isFocused: Bool?
    @State private var squareColor: Color = .gray
    @State private var colorName: String = "Gray"
    
    var body: some View {
        ZStack {
            Color.black
                .ignoresSafeArea()
            
            VStack(alignment: .center, spacing: 8) {
                
                Text("Press the **first letter** of a color to change the background!")
                    .font(.subheadline)
                    .foregroundColor(.gray)
                
                    .padding(.top, 40)
                
                Spacer()
                
                Text("You chose \(colorName)")
                    .font(.title2)
                    .fontWeight(.bold)
                    .foregroundColor(squareColor)
                    .padding(10)
                    .background(
                        RoundedRectangle(cornerRadius: 10)
                            .fill(Color.white.opacity(0.2))
                            .shadow(color: squareColor.opacity(0.7), radius: 8, x: 0, y: 4)
                    )
                    .padding(.bottom, 20)
                
                Spacer()
                VStack(alignment: .center, spacing: 10) {
                    ForEach(0..<20) { row in
                        HStack(spacing: 10) {
                            ForEach(0..<25) { column in
                                LightingSquare(color: $squareColor)
                            }
                        }
                        .offset(x: row % 2 == 0 ? 0 : 12.5, y: 0)
                    }
                }
                
                // Make the view focusable
                .focusable()
                .focused($isFocused, equals: true)
                
                // Implement the key press method
                .onKeyPress { key in
                    handleKeyPress(keyPress: key)
                    return .handled
                }
                Spacer()
            }
            .padding()
            .onAppear {
                isFocused = true
            }
        }
        
    }
    
    // Handle Key Press for Color Change
    private func handleKeyPress(keyPress: KeyPress) {
        switch keyPress.key {
        case KeyEquivalent("r"):
            squareColor = .red
            colorName = "Red"
        case KeyEquivalent("g"):
            squareColor = .green
            colorName = "Green"
        case KeyEquivalent("b"):
            squareColor = .blue
            colorName = "Blue"
        case KeyEquivalent("y"):
            squareColor = .yellow
            colorName = "Yellow"
        case KeyEquivalent("t"):
            squareColor = .teal
            colorName = "Teal"
        case KeyEquivalent("p"):
            squareColor = .pink
            colorName = "Pink"
        case KeyEquivalent("w"):
            squareColor = .white
            colorName = "White"
        case KeyEquivalent("m"):
            squareColor = .mint
            colorName = "Mint"
        case KeyEquivalent("o"):
            squareColor = .orange
            colorName = "Orange"
        default:
            break
        }
    }
}



struct LightingSquare: View {
    @Binding var color: Color
    @State private var isFlashing: Bool = false
    var body: some View {
        Rectangle()
            .fill(color)
            .frame(width: 15, height: 15)
            .opacity(isFlashing ? 0.8 : 0.3)
            .animation(
                Animation.easeInOut(duration: Double.random(in: 0.5...2.5)).repeatForever(autoreverses: true),
                value: isFlashing
            )
            .onAppear {
                isFlashing = true
            }
    }
}

In this view, a focusable grid of flashing squares dynamically changes its color based on the physical key pressed by the user: the onKeyPress method captures the specific key inputs to update the grid and background colors in real-time when it matches with any case provided.

0:00
/0:20