There are typically two approaches you might use to keep a view above the keyboard as it animates into place. As you know, the first is to listen for the UIKeyboardWillShowNotification
and use the accompanying duration/curve/frame values in the userData to help you position and animate your view above the keyboard.
A second approach is to supply an inputAccessoryView
for the view (UITextField
, here) that is invoking the keyboard. (I realize this won't provide the effect you're asking for, which is to "push" the toolbar/textfield up once the keyboard runs into it. But more on this later.) iOS will parent your inputAccessoryView to the view that also parents the keyboard and animate them in together. In my experience this provides the best-looking animation. I don't think I've ever had perfect animation using the UIKeyboardWillShowNotification
approach, especially now in iOS7 where there's a little bounce at the end of the keyboard animation. There's probably a way with UIKit Dynamics to apply this bounce to your view too, but making it perfectly in sync with the keyboard would be hard.
Here's what I've done in the past for a scenario similar to yours: there is bottom-positioned UIToolbar
having a UITextField
in a customView bar button item for input. In your case this is positioned above a UITabBar
. The ITextField
has a custom inputAccessoryView
set, which is another UIToolbar
with another UITextField
.
When the user taps into the text field and it becomes first responder, the keyboard animates into place with the 2nd toolbar/textfield along with it (and this transition looks very nice!). When we notice this happening we transition the firstResponder from the first text field to the second such that it has the blinking caret once the keyboard is in place.
The trick is what to do when you determine its time to end editing. First, you have to resignFirstResponder on the second text field, but if you're not careful then the system will pass first responder status back to the original text field! So you have to prevent that, because otherwise you'll be in an infinite loop of passing forward the first responder, and the keyboard will never dismiss. Second, you need to mirror any text input to the second text field back to the first text field.
Here's the code for this approach:
@implementation TSViewController
{
IBOutlet UIToolbar* _toolbar; // parented in your view somewhere
IBOutlet UITextField* _textField; // the customView of a UIBarButtonItem in the toolbar
IBOutlet UIToolbar* _inputAccessoryToolbar; // not parented. just owned by the view controller.
IBOutlet UITextField* _inputAccessoryTextField; // the customView of a UIBarButtonItem in the inputAccessoryToolbar
}
- (void) viewDidLoad
{
[super viewDidLoad];
_textField.delegate = self;
_inputAccessoryTextField.delegate = self;
_textField.inputAccessoryView = _inputAccessoryToolbar;
}
- (void) textFieldDidBeginEditing: (UITextField *) textField
{
if ( textField == _textField )
{
// can't change responder directly during textFieldDidBeginEditing. postpone:
dispatch_async(dispatch_get_main_queue(), ^{
_inputAccessoryTextField.text = textField.text;
[_inputAccessoryTextField becomeFirstResponder];
});
}
}
- (BOOL) textFieldShouldBeginEditing: (UITextField *) textField
{
if ( textField == _textField )
{
// only become first responder if the inputAccessoryTextField isn't the first responder.
return ![_inputAccessoryTextField isFirstResponder];
}
return YES;
}
- (void) textFieldDidEndEditing: (UITextField *) textField
{
if ( textField == _inputAccessoryTextField )
{
_textField.text = textField.text;
}
}
// invoke this when you want to dismiss the keyboard!
- (IBAction) done: (id) sender
{
[_inputAccessoryTextField resignFirstResponder];
}
@end
There's one final possibility I can think of. The approach above has the drawback of two separate toolbars/textfields. What you ideally want is just one set of these, and you want it to appear that the keyboard "pushes" them up (or pulls them down). In reality the animation is fast enough that I don't think most people would notice there are two sets for the above approach, but maybe you don't like that..
This final approach listens for the keyboard to show/hide, and uses a CADisplayLink
to synchronize animating the toolbar/textfield as it detects changes in the keyboard position in real time. In my tests it looks pretty good. The main drawback I see is that the positioning of the toolbar lags a tiny bit. I'm using auto-layout and changing over to traditional frame-positioning might be faster. Another drawback is there is a dependency on the keyboard view hierarchy not changing dramatically. This is probably the biggest risk.
There's one other trick with this. The toolbar is positioned in my storyboard using constraints. There are two constraints for the distance from the bottom of the view. One is tied to the IBOutlet "_toolbarBottomDistanceConstraint", and this is what the code uses to move the toolbar. This constraint is a "vertical space" constraint with a "Equal" relation. I set the priority to 500. There is a second parallel "vertical space" constraint with a "Greater than or equal" relation. The constant on this is the minimum distance to the bottom of the view (above your tab bar, for example), and the priority is 1000. With these two constraints in place I can set the toolbars distance-from-bottom to any value I like, but it will never drop below my minimum value. This is key to making it appear that the keyboard is pushing/pulling the toolbar, but having it "drop off" the animation at a certain point.
Finally, perhaps you could make a hybrid of this approach with what you've already got: use a CADisplayLink callback to detect when the keyboard has "run into" your toolbar, then instead of manually positioning the toolbar for the remainder of the animation, use a real UIView animation to animate your toolbar into place. You could set the duration to be the keyboard-display-animation-duration minus the time already transpired.
@implementation TSViewController
{
IBOutlet UITextField* _textField;
IBOutlet UIToolbar* _toolbar;
IBOutlet NSLayoutConstraint* _toolbarBottomDistanceConstraint;
CADisplayLink* _displayLink;
}
- (void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
}
- (void) viewDidLoad
{
[super viewDidLoad];
[self.view addGestureRecognizer: [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector( dismiss:) ]];
_textField.inputAccessoryView = [[UIView alloc] init];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(keyboardWillShowHide:)
name: UIKeyboardWillShowNotification
object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(keyboardWillShowHide:)
name: UIKeyboardWillHideNotification
object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(keyboardDidShowHide:)
name: UIKeyboardDidShowNotification
object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(keyboardDidShowHide:)
name: UIKeyboardDidHideNotification
object: nil];
}
- (void) keyboardWillShowHide: (NSNotification*) n
{
_displayLink = [CADisplayLink displayLinkWithTarget: self selector: @selector( tick: )];
[_displayLink addToRunLoop: [NSRunLoop currentRunLoop] forMode: NSRunLoopCommonModes];
}
- (void) keyboardDidShowHide: (NSNotification*) n
{
[_displayLink removeFromRunLoop: [NSRunLoop currentRunLoop] forMode: NSRunLoopCommonModes];
}
- (void) tick: (CADisplayLink*) dl
{
CGRect r = [_textField.inputAccessoryView.superview.layer.presentationLayer frame];
r = [self.view convertRect: r fromView: _textField.inputAccessoryView.superview.superview];
CGFloat fromBottom = self.view.bounds.size.height - r.origin.y;
_toolbarBottomDistanceConstraint.constant = fromBottom;
}
- (IBAction) dismiss: (id) sender
{
[self.view endEditing: YES];
}
@end
Here's the view hierarchy and constraints: