crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

HardwareKeyboardInputView.swift (5499B)


      1 import SwiftUI
      2 import UIKit
      3 
      4 struct HardwareKeyboardEvent {
      5     let keyCode: UIKeyboardHIDUsage
      6     let charactersIgnoringModifiers: String
      7     let modifierFlags: UIKeyModifierFlags
      8 }
      9 
     10 struct HardwareKeyboardInputView: UIViewRepresentable {
     11     var onPress: (HardwareKeyboardEvent) -> Bool
     12 
     13     func makeUIView(context: Context) -> KeyCaptureView {
     14         let view = KeyCaptureView()
     15         view.onPress = onPress
     16         return view
     17     }
     18 
     19     func updateUIView(_ uiView: KeyCaptureView, context: Context) {
     20         uiView.onPress = onPress
     21         uiView.ensureFirstResponder()
     22     }
     23 
     24     final class KeyCaptureView: UIView {
     25         var onPress: ((HardwareKeyboardEvent) -> Bool)?
     26 
     27         override var canBecomeFirstResponder: Bool { true }
     28 
     29         override var keyCommands: [UIKeyCommand]? {
     30             let letters = "abcdefghijklmnopqrstuvwxyz".map {
     31                 UIKeyCommand(
     32                     input: String($0),
     33                     modifierFlags: [],
     34                     action: #selector(handleKeyCommand(_:))
     35                 )
     36             }
     37 
     38             return letters + [
     39                 UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     40                 UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     41                 UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))),
     42                 UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))),
     43                 UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     44                 UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     45                 UIKeyCommand(input: "\u{8}", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     46                 UIKeyCommand(input: "\u{7F}", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     47                 UIKeyCommand(input: "\t", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     48                 UIKeyCommand(input: "\t", modifierFlags: .shift, action: #selector(handleKeyCommand(_:))),
     49                 UIKeyCommand(input: " ", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     50                 UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     51                 UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(handleKeyCommand(_:)))
     52             ]
     53         }
     54 
     55         override func didMoveToWindow() {
     56             super.didMoveToWindow()
     57             ensureFirstResponder()
     58         }
     59 
     60         func ensureFirstResponder() {
     61             guard window != nil, !isFirstResponder else { return }
     62             Task { @MainActor in
     63                 self.becomeFirstResponder()
     64             }
     65         }
     66 
     67         override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
     68             var unhandled: [UIPress] = []
     69 
     70             for press in presses {
     71                 guard let key = press.key else {
     72                     unhandled.append(press)
     73                     continue
     74                 }
     75 
     76                 let event = HardwareKeyboardEvent(
     77                     keyCode: key.keyCode,
     78                     charactersIgnoringModifiers: key.charactersIgnoringModifiers,
     79                     modifierFlags: key.modifierFlags
     80                 )
     81 
     82                 if onPress?(event) != true {
     83                     unhandled.append(press)
     84                 }
     85             }
     86 
     87             if !unhandled.isEmpty {
     88                 super.pressesBegan(Set(unhandled), with: event)
     89             }
     90         }
     91 
     92         @objc private func handleKeyCommand(_ command: UIKeyCommand) {
     93             guard let event = HardwareKeyboardEvent(
     94                 characters: command.input ?? "",
     95                 modifierFlags: command.modifierFlags
     96             ) else { return }
     97             _ = onPress?(event)
     98         }
     99     }
    100 }
    101 
    102 private extension HardwareKeyboardEvent {
    103     init?(characters: String, modifierFlags: UIKeyModifierFlags) {
    104         let keyCode: UIKeyboardHIDUsage
    105 
    106         switch characters {
    107         case UIKeyCommand.inputLeftArrow:
    108             keyCode = .keyboardLeftArrow
    109         case UIKeyCommand.inputRightArrow:
    110             keyCode = .keyboardRightArrow
    111         case UIKeyCommand.inputUpArrow:
    112             keyCode = .keyboardUpArrow
    113         case UIKeyCommand.inputDownArrow:
    114             keyCode = .keyboardDownArrow
    115         case UIKeyCommand.inputEscape:
    116             keyCode = .keyboardEscape
    117         case "\u{8}":
    118             keyCode = .keyboardDeleteOrBackspace
    119         case "\u{7F}":
    120             keyCode = .keyboardDeleteForward
    121         case "\t":
    122             keyCode = .keyboardTab
    123         case " ":
    124             keyCode = .keyboardSpacebar
    125         case "\r":
    126             keyCode = .keyboardReturnOrEnter
    127         case "a"..."z", "A"..."Z":
    128             guard let scalar = characters.uppercased().unicodeScalars.first,
    129                   let code = UIKeyboardHIDUsage(rawValue: Int(scalar.value - 65) + UIKeyboardHIDUsage.keyboardA.rawValue) else {
    130                 return nil
    131             }
    132             keyCode = code
    133         default:
    134             return nil
    135         }
    136 
    137         self.init(
    138             keyCode: keyCode,
    139             charactersIgnoringModifiers: characters,
    140             modifierFlags: modifierFlags
    141         )
    142     }
    143 }