Search code examples
iosswiftobjective-ccocoa-touch

Check if shift/cmd/option/caps-lock key is pressed in iOS app


I'm making an iOS application which can use hardware keyboard or run on Apple-Silicon macs. It seems that macos apps can take use of a very convenient APIs for checking if some of the modifier keys are pressed (How to check if Option key is down when user clicks a NSButton) like BOOL alt = [NSEvent modifierFlags] & NSAlternateKeyMask. This is exactly what I want, but I can't find anything like this for iOS-based app.

UIEvent has modifierFlags but only as an instance property.

NSEvent has both instance modifierFlags as well as static modifierFlags which can be used for querying the state anywhere inside the app without bothering with observing keyboard events.


Solution

  • You can write such a static property yourself, and update it as the key presses change, by overriding pressesBegan/pressesEnded in your app delegate/scene delegate (or anywhere else along the responder chain).

    extension UIEvent {
        fileprivate(set) static var modifierKeys: UIKeyModifierFlags = []
    }
    
    // ...
    
    override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        UIEvent.modifierKeys.formUnion(event?.modifierFlags ?? [])
    }
    
    override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        UIEvent.modifierKeys.formIntersection(event?.modifierFlags ?? [])
    }
    
    override func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        UIEvent.modifierKeys.formIntersection(event?.modifierFlags ?? [])
    }
    

    The above cannot get you the current state of the caps lock key until your app receives a key press. The only way I found is through private IOKit APIs (inspired by this answer). This will likely get your app rejected from the App Store, but I will include it anyway as a proof of concept.

    // ---- Implementation ----
    
    #include <IOKit/IOKitLib.h>
    
    #define kIOHIDSystemClass    "IOHIDSystem"
    #define kIOHIDParamConnectType 1
    #define kIOHIDCapsLockState 0x00000001
    
    // In macOS the above constants and IOHIDGetModifierLockState
    // are declared in IOKit/hid/IOHIDLib.h but that header is not in iOS.
    // Even so, IOHIDGetModifierLockState apparently *does* exist in iOS
    // so we can just declare its prototype and linking succeeds nonetheless
    
    extern kern_return_t
    IOHIDGetModifierLockState( io_connect_t handle, int selector, bool *state );
    
    @implementation ObjCClass
     
    + (BOOL)getCapsLock {
        bool state = false;
        @autoreleasepool {
            io_service_t ioService = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching(kIOHIDSystemClass));
            io_connect_t ioConnect = 0;
            IOServiceOpen(ioService, mach_task_self_, kIOHIDParamConnectType, &ioConnect);
    
            IOHIDGetModifierLockState(ioConnect, kIOHIDCapsLockState, &state);
    
            IOServiceClose(ioConnect);
        }
        return state;
    }
    
    @end
    
    // ---- Swift Bridging Header ----
    
    @interface ObjCClass : NSObject
     
    + (BOOL)getCapsLock;
    
    @end
    

    You can then just do ObjCClass.getCapsLock().