iosuiresponderuikeycommand

UIResponder: keyCommands not called; confirmed isFirstResponder


My app has a passcode view, conceptually similar to the iOS unlock screen. It's a UIViewController that I present within a new UIWindow. Works fine. I'm adding the capability to type the passcode using a hardware keyboard. The keyCommands method is not called, thus key presses not recognized, until the user taps anywhere on the screen at least once. It's a full-screen UIWindow/UIViewController so presumably it's a tap within the UIWindow/UIViewController. Once that tap occurs, keyCommands will be called as expected, and all works perfectly. I don't want to require the user to tap their screen before typing their passcode.

Any idea what's going on here, specifically why the user needs to tap the screen (and how to avoid that requirement)?

I have verified the UIViewController is the firstResponder by including a repeating NSTimer call to verify as such.

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self becomeFirstResponder];
    [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) { //This is just debugging code!
        [self confirmFirstResponder];
    }];
}

-(BOOL)canBecomeFirstResponder {
    return YES;
}

-(void)confirmFirstResponder { //Caveman debugging at its finest
    if ([self isFirstResponder]) {
        NSLog(@"I'm first responder!"); //This is always logged repeatedly
    } else {
        NSLog(@"I'm NOT THE FIRST RESPONDER!!!!"); //This is never logged
    }
}

-(NSArray<UIKeyCommand *> *)keyCommands {
    NSLog(@"keyCommands fired"); //This is not fired until user taps the screen, then presses a key on the hardware keyboard
    NSArray<UIKeyCommand *> *commands = @[
        [UIKeyCommand commandWithTitle:@"1" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"1" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"2" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"2" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"3" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"3" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"4" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"4" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"5" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"5" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"6" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"6" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"7" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"7" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"8" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"8" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"9" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"9" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"0" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"0" modifierFlags:0 propertyList:nil],
        [UIKeyCommand commandWithTitle:@"Delete" image:nil action:@selector(buttonPressedWithKeyCommand:) input:@"\b" modifierFlags:0 propertyList:nil]
    ];
    return commands;
}

Here's the code to create the UIWindow:

-(void)displayNewWindowWithViewController:(UIViewController *)vc {
    UINavigationController *nav=[[UINavigationController alloc] initWithRootViewController:vc]; //vc is the UIViewController containing the code above
    nav.navigationBarHidden=YES; //I have no recollection why I'm wrapping the vc in a UINavigationController...
    self.modalWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.modalWindow.backgroundColor = [UIColor whiteColor];
    self.modalWindow.clipsToBounds = NO;
    self.modalWindow.rootViewController = nav;
    self.modalWindow.windowLevel=99;
    [self makeKeyAndVisible:self.modalWindow];
}

-(void)makeKeyAndVisible:(UIWindow *)window {
    window.backgroundColor = [UIColor clearColor];
    window.frame=CGRectMake(0, [UIScreen mainScreen].bounds.size.height, window.frame.size.width, window.frame.size.height);
    [window makeKeyAndVisible];
    window.frame=CGRectMake(0, 0, window.frame.size.width, window.frame.size.height);
}

Other than the hardware keyboard code I'm asking about, I wrote everything here nearly seven years ago. So I don't remember the specific logic for...anything. I do know I used a UIWindow because this is a security view, which absolutely must be over all other views, including some that the app may be adding while this is visible. Ideal or not, it's been working perfectly. If a substantive rearchitecture is needed to make the hardware keyboard work here, the hardware keyboard capability will be dropped.


Solution

  • The new UIWindow wasn't quite the "key window" in terms of capturing keyboard events, even with the makeKeyandVisible instruction. I verified this by temporarily adding the same code to a UIViewController on the app's main UIWindow. It received the keyboard events until I tapped the screen (the new UIWindow).

    I changed this:

    [window makeKeyAndVisible]
    

    to this:

    window.hidden = NO;
    dispatch_async(dispatch_get_main_queue(), ^{
        [window makeKeyWindow];
    });
    

    and suddenly the keyboard was fully captured by my new UIWindow.

    I'm not quite sure what was going on. The original [window makeKeyAndVisible] was definitely running on the main thread (verified with [NSThread isMainThread]). But throwing it into another run loop did the trick. Go figure.