Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIKeyboard avoidance and Auto Layout

That blog post is great, but I'd like to suggest some improvements to it. First, you can register to observe frame changes, so you don't need to register to observe both show and hide notifications. Second, you should convert the CGRects for the keyboard from screen to view coordinates. Last, you can copy the exact animation curve used by iOS for the keyboard itself, so the keyboard and the tracking views move in synchrony.

Putting it all together, you get the following:

@interface MyViewController ()
// This IBOutlet holds a reference to the bottom vertical spacer
// constraint that positions the "tracking view",i.e., the view that
// we want to track the vertical motion of the keyboard
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomVerticalSpacerConstraint;
@end


@implementation MyViewController
-(void)viewDidLoad
{
  [super viewDidLoad];
  // register for notifications about the keyboard changing frame
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(keyboardWillChangeFrame:)
                                               name:UIKeyboardWillChangeFrameNotification
                                             object:self.view.window];
}

-(void)keyboardWillChangeFrame:(NSNotification*)notification
{
  NSDictionary * userInfo = notification.userInfo;
  UIViewAnimationCurve animationCurve  = [userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue];
  NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];

  // convert the keyboard's CGRect from screen coords to view coords
  CGRect kbEndFrame = [self.view convertRect:[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]
                                    fromView:self.view.window];
  CGRect kbBeginFrame = [self.view convertRect:[[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]
                                      fromView:self.view.window];
  CGFloat deltaKeyBoardOrigin = kbEndFrame.origin.y - kbBeginFrame.origin.y;

  // update the constant factor of the constraint governing your tracking view
  self.bottomVerticalSpacerConstraint.constant -= deltaKeyBoardOrigin;
  // tell the constraint solver it needs to re-solve other constraints.
  [self.view setNeedsUpdateConstraints];

  [UIView beginAnimations:nil context:NULL];
  [UIView setAnimationDuration:duration];
  [UIView setAnimationCurve:animationCurve];
  [UIView setAnimationBeginsFromCurrentState:YES];
  // within this animation block, force the layout engine to apply 
  // the new layout changes immediately, so that we
  // animate to that new layout. We need to use old-style
  // UIView animations to pass the curve type.
  [self.view layoutIfNeeded];
  [UIView commitAnimations];
}

-(void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self
                                                  name:UIKeyboardWillChangeFrameNotification
                                                object:nil];
}
@end

This will work, as long as you don't change orientation while the keyboard is up.

It was an answer on How to mimic Keyboard animation on iOS 7 to add "Done" button to numeric keyboard? showed how to mimic the keyboard animation curve correctly.

One last thing to beware of with respect to all these notification-based solutions: they can produce unexpected effects if some other screen in your app also uses the keyboard, because your view controller will still receive the notifications as long as it has not been deallocated, even if it's views are unloaded. One remedy for this is to put a conditional in the notification handler to ensure it only operates when the view controller is on screen.


Using the KeyboardLayoutConstraint in the Spring framework is the simplest solution I've found so far. KeyboardLayoutConstraint


My idea is to create a UIView, let's call it keyboard view, and place it to your view controller's view. Then observe keyboard frame change notifications UIKeyboardDidChangeFrameNotification and match the frame of the keyboard to the keyboard view (I recommend to animate the change). Observing this notification handles the rotation you mentioned and also moving keyboard on iPad.

Then simply create your constraints relative to this keyboard view. Don't forget to add the constraint to their common superview.

To get the keyboard frame correctly translated and rotated to your view coordinates check out the docs for UIKeyboardFrameEndUserInfoKey.


I created a view like this that would watch the keyboard and change its own constraints when the keyboard comes on/off the screen.

@interface YMKeyboardLayoutHelperView ()
@property (nonatomic) CGFloat desiredHeight;
@property (nonatomic) CGFloat duration;
@end

@implementation YMKeyboardLayoutHelperView

- (id)init
{
    self = [super init];
    if (self) {
        self.translatesAutoresizingMaskIntoConstraints = NO;

        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:@"UIKeyboardWillShowNotification" object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:@"UIKeyboardWillHideNotification" object:nil];
    }
    return self;
}

- (void)keyboardWillShow:(NSNotification *)notification
{
    // Save the height of keyboard and animation duration
    NSDictionary *userInfo = [notification userInfo];
    CGRect keyboardRect = [userInfo[@"UIKeyboardBoundsUserInfoKey"] CGRectValue];
    self.desiredHeight = CGRectGetHeight(keyboardRect);
    self.duration = [userInfo[@"UIKeyboardAnimationDurationUserInfoKey"] floatValue];

    [self setNeedsUpdateConstraints];
}

- (void)keyboardWillHide:(NSNotification *)notification
{
    // Reset the desired height (keep the duration)
    self.desiredHeight = 0.0f;

    [self setNeedsUpdateConstraints];
}

- (void)updateConstraints
{
    [super updateConstraints];

    // Remove old constraints
    if ([self.constraints count]) {
        [self removeConstraints:self.constraints];
    }

    // Add new constraint with desired height
    NSString *constraintFormat = [NSString stringWithFormat:@"V:[self(%f)]", self.desiredHeight];
    [self addVisualConstraints:constraintFormat views:@{@"self": self}];

    // Animate transition
    [UIView animateWithDuration:self.duration animations:^{
        [self.superview layoutIfNeeded];
    }];

}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end

Ive written a library that will do it all for you (supports Auto Layout and Springs & Struts)

IHKeyboardAvoiding https://github.com/IdleHandsApps/IHKeyboardAvoiding

Just call [IHKeyboardAvoiding setAvoidingView:self.myView];