27

Given the focus on Auto Layout in iOS 6, and the recommendation by Apple engineers (see WWDC 2012 videos) that we no longer manipulate a views' frame directly, how would one go about avoiding the keyboard using only Auto Layout and NSLayoutConstraint?

Update

This looks like a reasonable solution: An example of keyboard sensitive layout (GitHub source) but one potential issue I see is what happens when a user rotates the device and the keyboard is already on screen?

  • Taking the example from the link, I would refactor the code out for calculating the `keyboardHeight` and add `willRotateToInterfaceOrientation:` and/or `willAnimateRotationToInterfaceOrientation:` calls as appropriate to update the keyboard height constraint. – Michael Kernahan Oct 24 '12 at 17:14
  • 1
    The `UIKeyboardWillShowNotification` notification is also posted when the size of the keyboard changes due to rotation, so you don't have to worry about that. – omz Nov 28 '12 at 21:13
  • +1 for the interesting link – memmons Mar 22 '13 at 03:11

6 Answers6

7

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.

Community
  • 1
  • 1
algal
  • 27,584
  • 13
  • 78
  • 80
  • Many notifications should be registered in viewWillAppear and unregistered in viewWillDisappear. – Steven Kramer Aug 28 '14 at 22:01
  • @StevenKramer It's my impression that (1) viewWillAppear and viewWillDisappear are not reliably balanced calls, and (2) that viewWillAppear in particular is not called in many circumstances when a view is made visible (app resumes active, etc.). So I'm not sure how best to use them to manage notifications. Did you have something specific in mind? – algal Sep 01 '14 at 05:26
  • That used to happen a lot, but it's not such a problem these days (iOS 5+). In fact, if you still have unbalanced appearance messages, I'd say you should fix that immediately. Or work around it, if it's a framework issue. – Steven Kramer Sep 01 '14 at 07:26
  • Oh, wrt to "app resumes active". It is in fact correct to not get `viewWill(Dis)Appear` messages. The "appearance" alluded to is the appearance of the view in your application windows, not on the device screen. – Steven Kramer Sep 01 '14 at 07:28
  • For me I'm on iOS 8 and the message comes in twice. Once with the frame coming into my view and a second notifying me of it coming into position outside of the view. So the -= is throwing it off. I had to add a check for the position to see if it was beyond the size of the view/screen [code] !(kbBeginFrame.origin.y > self.window.frame.size.height || kbEndFrame.origin.y > self.window.frame.size.height);[/code] – cynistersix Feb 13 '15 at 18:36
7

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

James Tang
  • 995
  • 1
  • 10
  • 17
  • 1
    +1, not because this is the best solution for all cases but if your project is already using the Spring framework, it is the way to go. – Eneko Alonso Apr 23 '15 at 21:04
  • No need to use the framework, just copy the KeyboardLayoutConstraint.swift file into your project and use it at will – xtrinch Apr 19 '16 at 15:45
  • Word of caution to those using this tactic, KeyboardLayoutConstraint doesn't handle bluetooth keyboard notifications properly. See here for solution: http://stackoverflow.com/questions/32319697/view-aligned-to-top-of-keyboard-appears-in-wrong-place-in-ios-9-shortcut-bar-onl – elprl Jun 30 '16 at 17:10
6

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.

Tricertops
  • 8,492
  • 1
  • 39
  • 41
  • Using a proxy view in this way works really well; great idea! –  Nov 30 '12 at 13:36
  • 1
    i have implemented such a view, fill free to use it :3 https://gist.github.com/safareli/d9270e7faaad3f8d151c – Safareli Nov 10 '14 at 13:50
  • 1
    Ive written a library that will do it all for you (supports Auto Layout and Springs & Struts) https://github.com/IdleHandsApps/IHKeyboardAvoiding – Fraser Dec 15 '14 at 00:50
5

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
Steven Hepting
  • 12,394
  • 8
  • 40
  • 50
1

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];

Fraser
  • 953
  • 11
  • 21
0

For auto layout with keyboard case, I use static table view. This keeps your codes much simpler and not need to keep track of keyboard height. One thing I learned about table view is to keep each table row as narrow as possible. If you put too many UIs vertically in one row, you may get keyboard overlap.

David.Chu.ca
  • 37,408
  • 63
  • 148
  • 190