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.
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.
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
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With