6

Similar to this question: iPad: Detecting External Keyboard, I am developing an iPad app that is using text fields with a custom inputAccessoryView to provide additional functionality for the virtual keyboard.

However, if a hardware keyboard (e.g. bluetooth keyboard) is connected to the device, the software keyboard is not shown as expected, but for some reason the inputAccessoryView is still visible at the bottom of the screen. Additionally, this seems to cause firing the UIKeyboardDidShowNotification(and therefore moving my view up to avoid occlusion by the keyboard which is actually not present) even if the hardware keyboard is used for input.

I found several solutions to detect if a hardware keyboard is connected, but all of them check the state after receiving a UIKeyboardDidShowNotification, at which point the inputAccessoryView is already visible (e.g. How can I detect if an external keyboard is present on an iPad?).

I am looking for a way to only show a inputAccessoryView if there is no hardware keyboard connected. Therefore I need to know if a hardware keyboard is connected before a UIKeyboardDidShowNotification is fired.

The accepted solutions provided here How can I detect if an external keyboard is present on an iPad? are no option for me as they use private APIs which may cause my app to get rejected.

Community
  • 1
  • 1
Rob
  • 491
  • 4
  • 14
  • Is the issue that you don't want the input accessory view at all with a hardware keyboard or is the issue that the screen elements are moving as if there is a software keyboard even though there is only the accessory view? If the latter, be sure you only move the views enough for the accessory view and not some hardcode height of the software keyboard. – rmaddy Jan 08 '15 at 00:05
  • I don't want the input accessory view **at all** with a hardware keyboard, the second point should then become irrelevant anyway because no more notifications are being sent – Rob Jan 08 '15 at 11:58
  • 1
    @RobK Actually, notifications **are** still sent when an external keyboard is attached. When a `UITextView`/`Field` becomes active, you'll get the notification, it's just that the system keeps the keyboard off-screen. – mbm29414 Feb 17 '15 at 13:55
  • @mbm29414 you are right, that was a false assumption by me. Nonetheless I am now using a different approach to calculate only the **visible** portion of the keyboard by using the `origin.y` of `UIKeyboardFrameEndUserInfoKey`. – Rob Feb 25 '15 at 14:58
  • @RobK That's a good way. – mbm29414 Feb 25 '15 at 14:59
  • I got to correct myself again: there are indeed **no notifications sent** if a hardware keyboard is attached and the `inputAccessoryView` of the active text field is `nil` – Rob Feb 26 '15 at 01:49

4 Answers4

4

This is just an enhancement of the answer by @arlomedia. What I did was watch for the willShow and didShow.

The willShow I use to move my textview into position so that it moves at the same rate as the keyboard.

The didShow I use to check the apparent size of the keyboard using the aforementioned technique and hide/show the accessoryInputView accordingly.

It's important that I also set that view to be hidden by default, and that when a willHide event is received, it is hidden again then.

- (void) addKeyboardObserver {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardHidden:) name:UIKeyboardWillHideNotification object:nil];
}

- (void) removeKeyboardObserver {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

- (void)keyboardWillShow:(NSNotification*)notification {
    CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;

    // If we're on iOS7 or earlier and landscape then the height is in the
    // width.
    //
    if ((IS_LANDSCAPE == YES) && (IS_IOS8_OR_LATER == NO)) {
        keyboardSize.height = keyboardSize.width;
    }

    NSNumber *rate = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey];

    CGRect textFieldFrame = self.textField.frame;
    textFieldFrame.origin.y = ([Util screenHeight] - keyboardSize.height) - textFieldFrame.size.height - [Util scaledHeight:10.0];

    // Move the text field into place.
    //
    [UIView animateWithDuration:rate.floatValue animations:^{
        self.answerTextField.frame = textFieldFrame;
    }];

    keyboardShown = YES;
}

- (void)keyboardDidShow:(NSNotification*)notification {
    CGRect keyboardBeginFrame = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
    CGRect keyboardEndFrame = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    CGSize keyboardSize = keyboardBeginFrame.size;

    // If we're on iOS7 or earlier and landscape then the height is in the
    // width.
    //
    if ((IS_LANDSCAPE == YES) && (IS_IOS8_OR_LATER == NO)) {
        keyboardSize.height = ABS(keyboardBeginFrame.origin.x - keyboardEndFrame.origin.x); // the keyboard will move by an amount equal to its height when it appears; ABS is needed for upside-down orientations
    } else {
        keyboardSize.height = ABS(keyboardBeginFrame.origin.y - keyboardEndFrame.origin.y); // the keyboard will move by an amount equal to its height when it appears; ABS is needed for upside-down orientations
    }

    NSNumber *rate = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey];

    [UIView animateWithDuration:rate.floatValue animations:^{
        if (keyboardSize.height <= self.accessoryBar.frame.size.height) {
            self.textField.inputAccessoryView.hidden = YES;
            self.answerTextField.inputAccessoryView.userInteractionEnabled = NO;
        } else {
            self.textField.inputAccessoryView.hidden = NO;
            self.answerTextField.inputAccessoryView.userInteractionEnabled = YES;
        }
    }];

    keyboardShown = YES;
}

- (void)keyboardHidden:(NSNotification*)notification {

    NSNumber *rate = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey];

    // Remove/hide the accessory view so that next time the text field gets focus, if a hardware
    // keyboard is used, the accessory bar is not shown.
    //
    [UIView animateWithDuration:rate.floatValue animations:^{
        self.textField.inputAccessoryView.hidden = YES;
        self.answerTextField.inputAccessoryView.userInteractionEnabled = NO;
    }];

    keyboardShown = NO;
}

NOTE Edited to add change to userInteractionEnabled so that a hidden accessoryView doesn't eat taps.

PKCLsoft
  • 1,359
  • 2
  • 26
  • 35
  • Switching between `UIKeyboardDidShowNotification` and `UIKeyboardWillShowNotification` didn't make any difference for me. Your provided solutions of hiding the accessory view by default actually just turned my problem around in the way that the input accessory view will now not be shown **at all** since ` self.textField.inputAccessoryView.hidden = NO;` will not apply after the `UIKeyboardDidShowNotification` (respectively `UIKeyboardWillShowNotification`) has already been sent – Rob Feb 25 '15 at 14:47
  • Are you wrapping your changes to the hidden property in a UIView animation like I did. The timing of this may be what helps it to work, as this code is exactly what I have in my current project. – PKCLsoft Feb 26 '15 at 00:59
  • 1
    I found out that it actually did not have anything to do with the animation (can be omitted), but your solution hinted me to my mistake that caused all those issues. It had to do with me dynamically creating the inputAccessoryView (and inadvertently re-create it with every getter call). I will write a new answer to clear this up. – Rob Feb 26 '15 at 02:08
2

IIRC, views don't resize themselves when the software keyboard appears. I am resizing my views in a keyboardDidShow method triggered by a UIKeyboardDidShow notification. So it should be enough to detect the hardware vs. software keyboard in that method and then you could skip the table resizing and hide the input accessory view (or adjust the table resizing to accommodate the input accessory view, if you prefer to leave that visible).

To resize the views correctly whether or not a hardware keyboard is present, I adapted the code from this answer:

- (void)keyboardDidShow:(NSNotification *)aNotification {
    CGRect keyboardBeginFrame = [[[aNotification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
    CGRect keyboardEndFrame = [[[aNotification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    float keyboardHeight = ABS(keyboardBeginFrame.origin.y - keyboardEndFrame.origin.y); // the keyboard will move by an amount equal to its height when it appears; ABS is needed for upside-down orientations

    // now you can resize your views based on keyboardHeight
    // that will be the height of the inputAccessoryView if a hardware keyboard is present
}

That's all you need if you want to leave the inputAccessoryView visible. To also hide that, I think you will need to set an instance variable so you can access it in keyboardDidShow:

UIView *currentInputAccessoryView;

- (void)textFieldDidBeginEditing:(UITextField *)textField {
    self.currentInputAccessoryView = textField.inputAccessoryView;
}

- (void)textViewDidBeginEditing:(UITextView *)textView {
    self.currentInputAccessoryView = textView.inputAccessoryView;
}

- (void)keyboardDidShow:(NSNotification *)aNotification {
    CGRect keyboardBeginFrame = [[[aNotification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
    CGRect keyboardEndFrame = [[[aNotification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    float keyboardHeight = ABS(keyboardBeginFrame.origin.y - keyboardEndFrame.origin.y); // the keyboard will move by an amount equal to its height when it appears; ABS is needed for upside-down orientations

    if (keyboardHeight == 44) {
        self.currentInputAccessoryView.hidden = YES;
        keyboardHeight = 0;
    }

    // now you can resize your views based on keyboardHeight
    // that will be 0 if a hardware keyboard is present
}
Community
  • 1
  • 1
arlomedia
  • 8,534
  • 5
  • 60
  • 108
1

My final way to solve this issue was to simply add an observer for the UIKeyboardWillShowNotification ...

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillShow:)
                                             name:UIKeyboardWillShowNotification object:nil];

.. and hide the inputAccessoryView previously stored in an instance variable.

// Called when the UIKeyboardWillShowNotification is sent.
- (void)keyboardWillShow:(NSNotification*)notification
{
    NSLog(@"keyboardWillShow");

    // get the frame end user info key
    CGRect kbEndFrame = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

    // calculate the visible portion of the keyboard on the screen
    CGFloat height = [[UIScreen mainScreen] bounds].size.height - kbEndFrame.origin.y;

    // check if there is a input accessorry view (and no keyboard visible, e.g. hardware keyboard)
    if (self.activeTextField && height <= self.activeTextField.inputAccessoryView.frame.size.height) {

        NSLog(@"hardware keyboard");

        self.activeTextField.inputAccessoryView.hidden = YES;
    } else {

        NSLog(@"software keyboard");

        self.activeTextField.inputAccessoryView.hidden = NO;
    }
}

It turned out the problem was caused by me dynamically creating the inputAccessoryView of my custom UITextField subclass in its getter method. I inadvertently recreated the view with every call of the getter instead of reusing an instance variable with lazy instantiation. This resulted in all my assignments to the view being ignored since apparently the getter method will be called multiple times when the text field is being accessed and the keyboard shown and therefore the view kept being overridden after my assignments. Reusing the view by saving it to an instance variable fixed this issue.

Rob
  • 491
  • 4
  • 14
1

This is an old thread, but as of iOS 14 we now have proper APIs for tracking hardware keyboards via GameController framework using GCKeyboard and GCKeyboardDidConnect/GCKeyboardDidDisconnect notifications.

In this particular case of hiding and showing inputAccessoryView, you could do something like this:

import GameController

class ViewController: UIViewController {
    ...
    var isHardwareKeyboardConnected: Bool {
        didSet {
            enableInputAccessoryIfNeeded()
        }
    }

    func enableInputAccessoryIfNeeded() {
        textField.inputAccessoryView = isHardwareKeyboardConnected ? nil : textFielInputAccessoryView
        textField.reloadInputViews()
    }

    init() {
        isHardwareKeyboardConnected = GCKeyboard.coalesced != nil
        super.init(nibName: nil, bundle: nil)
        startObservingHardwareKeyboard()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        enableInputAccessoryIfNeeded()
    }

    func startObservingHardwareKeyboard() {
        NotificationCenter.default.addObserver(self, selector: #selector(hardwareKeyboardDidConnect), name: .GCKeyboardDidConnect, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(hardwareKeyboardDidDisconnect), name: .GCKeyboardDidDisconnect, object: nil)
    }

    @objc func hardwareKeyboardDidConnect(_ notification: Notification) {
        print("[Keyboard] Hardware keyboard did connect")
        isHardwareKeyboardConnected = true
    }

    @objc func hardwareKeyboardDidDisconnect(_ notification: Notification) {
        print("[Keyboard] Hardware keyboard did disconnect")
        isHardwareKeyboardConnected = false
    }
}
Alex Staravoitau
  • 2,222
  • 21
  • 27