Skip to content

Instantly share code, notes, and snippets.

@BigZaphod
Last active December 25, 2022 19:07
Show Gist options
  • Save BigZaphod/82e5b1c72ff5c032a41d8b82e4a9bf6b to your computer and use it in GitHub Desktop.
Save BigZaphod/82e5b1c72ff5c032a41d8b82e4a9bf6b to your computer and use it in GitHub Desktop.
A simple UIView that will move the contentView out of the way of the keyboard. It animates in sync with the keyboard and always works - even in a modal page sheet, a slide over, or any other context. Tested on iOS 12 and iOS 13.
// Created by Sean Heber on 9/26/19.
@import UIKit;
@interface KeyboardAwareContainerView : UIView
@property (nonatomic, readonly, nonnull) UIView *contentView;
@end
// Created by Sean Heber on 9/26/19.
#import "KeyboardAwareContainerView.h"
// keep the last known keyboard frame
// this is so that when a new container view is created while the keyboard is visible it can be correctly offset
static CGRect __keyboardFrame;
@implementation KeyboardAwareContainerView {
NSLayoutConstraint *_constraint;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self=[super initWithFrame:frame])) {
_contentView = [[UIView alloc] initWithFrame:self.bounds];
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_contentView];
[_contentView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
[_contentView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
[_contentView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
// we want to constrain the contentView to the bottom of the container view itself normally, but
// the priority is set to one less than Required so that when the keyboard is visible, that
// constraint will push it up away from the bottom edge if necessary.
NSLayoutConstraint *bottom = [_contentView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor];
bottom.priority = UILayoutPriorityRequired - 1;
bottom.active = YES;
// monitor the keyboard's movements
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrameNotification:) name:UIKeyboardWillChangeFrameNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
// install the constraint that keeps the view above the keyboard
_constraint = [self.window.bottomAnchor constraintGreaterThanOrEqualToAnchor:_contentView.bottomAnchor];
_constraint.constant = self.keyboardBottomOffset;
_constraint.active = YES;
}
- (CGFloat)keyboardBottomOffset
{
// find the overlap of the keyboard with our window
// NOTE: this assumes the window is never going to move while we're interested in this - probably a safe assumption on iOS, I think
// NOTE: converting the keyboard's frame MUST be done directly with the screen's coordinateSpace - simply using fromWindow:nil does
// not work correctly when the app is in side-over mode! (Probably a UIKit bug.)
const CGRect windowRect = [self.window convertRect:__keyboardFrame fromCoordinateSpace:self.window.screen.coordinateSpace];
const CGRect windowIntersection = CGRectIntersection(self.window.bounds, windowRect);
return CGRectIsNull(windowIntersection) ? 0 : windowIntersection.size.height;
}
- (void)keyboardWillChangeFrameNotification:(NSNotification *)notification
{
__keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
const NSTimeInterval animDuration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
const NSUInteger curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue]; // UIViewAnimationCurve
const UIViewAnimationOptions animCurve = (curve << 16); // convert it to the options format
// ensure we have a stable layout before adjusting the constraint so we don't animate things we didn't intend to
[UIView performWithoutAnimation:^{
[self layoutIfNeeded];
}];
// animate the change
[UIView animateWithDuration:animDuration delay:0 options:animCurve animations:^{
// update the constraint
_constraint.active = YES;
_constraint.constant = self.keyboardBottomOffset;
// force the view to layout again - otherwise the change won't be animated the way we'd like
[self layoutIfNeeded];
} completion:nil];
}
- (void)keyboardWillHideNotification:(NSNotification *)notification
{
// deactivate the constraint entirely so it doesn't interfere
// NOTE: this prevents a problem when dismissing a modal view controller that still had a keyboard presented
// in that case, iOS will dismiss the keyboard for you along with the dismiss animation, but if we don't
// immediately disable the constraint, then while the keyboard is animating down along with the view, our
// constraint would push the bottom of the contentView up - which looks terrible and is bad.
_constraint.active = NO;
}
@end
@BigZaphod
Copy link
Author

This should be pretty easy to convert to Swift (or use as-is). Put your views in the contentView and/or make your constraints relative to it and this should take care of everything else. This also works fine for older codebases that aren't using auto layout, of course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment