Skip to content

Instantly share code, notes, and snippets.

@TimMedcalf
Last active May 6, 2024 14:21
Show Gist options
  • Save TimMedcalf/9505416 to your computer and use it in GitHub Desktop.
Save TimMedcalf/9505416 to your computer and use it in GitHub Desktop.
The easy & reliable way of handling UITableView insets when the keyboard is shown. This works unchanged no matter where the table view is on the screen (including dealing with orientation, hierarchy, container view controllers & all devices)
/*
One of the first things someone new to iOS Development finds is that dealing with the keyboard is trickier
than they think it should be. Simply changing the scrolling extents of a UITableView (or UIScrollView, or
UICollectionView) that is partially covered by they keyboard reveals a lot about the internals of how iOS
works and highlights various "gotchas" that need to be considered.
There are various ways to know that a keyboard has been shown - but observing some specific notifications
provides a reliable way to allow you to modify your views to deal with it.
Even once you've captured that, there's an evolution to go through before finding something that works every
time.
e.g.
- "The keyboard is always 216 points high".
(No it isn't! Orientation and locale are just two of the things that affect this)
- "The keyboard height can be read from the notification dictionary."
(Correct, but be aware that it's always reported in portrait. Sometimes you'll want the height, sometimes
you'll want the width)
- "Now I've got the real height, I just need to subtract it from the bottom of my view"
(almost...)
- "Okay, my scrollable view, doesn't reach the bottom of the screen - so I just need to find the lowest point
of my view, compare it to the bottom of my viewController, subtract the difference from the keyboard height
and take that away from my scrollable area."
(I see where you're going - but it's starting to get complicated - and not very re-usable.)
- "Oh. My table is embedded in a view hierarchy that's got lots of layers. The CustomContainerViewController
implementation means that I don't easily know where it is on the screen. I need to do lots of manual conversions
betweeen different coordinate systems. Erm...viewA.rect.size.height + viewA.rect,origin.y + viewB.origin.y...
oh wait, what if viewC is present we'll need to deal with taking that bit off the bottom...erm...wow...are we in
view coordinates or screen coordinates? There must be something easier than this!"
(Yep, your head will hurt - while writing code that will probably be very specific to your current ViewController
implementation).
After several years coding in iOS I finally settled on the code below (I'm a slow learner). Its the simplest way
of reliably finding the values I need, while making me feel smart because I used functions like
"CGRectIntersection".
AFAIK, this works all the time. Handling device, orientation and where your tableView (and ViewController)
actually is in the overall view hierarchy.
Got a better way? Cool - tell me! I'd love to hear it.
Tim Medcalf
@timmedcalf
[email protected]
*/
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
/*
your code here
*/
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
/*
your code here
*/
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[super viewWillDisappear:animated];
}
- (void)keyboardWillShow:(NSNotification *)notification {
//get the end position keyboard frame
NSDictionary *keyInfo = [notification userInfo];
CGRect keyboardFrame = [[keyInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
//CGRect keyboardFrame = [[keyInfo objectForKey:@"UIKeyboardFrameEndUserInfoKey"] CGRectValue];
//convert it to the same view coords as the tableView it might be occluding
keyboardFrame = [self.myTableView convertRect:keyboardFrame fromView:nil];
//calculate if the rects intersect
CGRect intersect = CGRectIntersection(keyboardFrame, self.myTableView.bounds);
if (!CGRectIsNull(intersect)) {
//yes they do - adjust the insets on tableview to handle it
//first get the duration of the keyboard appearance animation
NSTimeInterval duration = [[keyInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// adjust the animation curve - untested
NSInteger curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue] << 16;
//change the table insets to match - animated to the same duration of the keyboard appearance
[UIView animateWithDuration:duration delay:0.0 options:curve animations: ^{
self.myTableView.contentInset = UIEdgeInsetsMake(0, 0, intersect.size.height, 0);
self.myTableView.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, intersect.size.height, 0);
} completion:nil];
}
}
- (void) keyboardWillHide: (NSNotification *) notification{
NSDictionary *keyInfo = [notification userInfo];
NSTimeInterval duration = [[keyInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
NSInteger curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue] << 16;
//change the table insets to match - animated to the same duration of the keyboard appearance
[UIView animateWithDuration:duration delay:0.0 options:curve animations: ^{
self.myTableView.contentInset = UIEdgeInsetsZero;
self.myTableView.scrollIndicatorInsets = UIEdgeInsetsZero;
} completion:nil];
}
@Jession
Copy link

Jession commented May 27, 2014

IOS7 is OK, but in IOS6, nothing happens.

@LukasCZ
Copy link

LukasCZ commented Feb 27, 2015

Hey Tim,
your code looks really bullet proof, good work! Though it didn't work for me on iOS 8 (the table insets remained changed even after the keyboard was hidden).

You were asking about better way; and I have something: You could use the modern Objective-C syntax for accessing dictionary objects, like this:

NSTimeInterval animDuration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];

Also, I would use the constants as the keys, instead of the raw strings:
this: UIKeyboardAnimationDurationUserInfoKey instead of this: @"UIKeyboardAnimationDurationUserInfoKey"

Lastly, you can use the same curve as the keyboard animation has for your animation. This is done using:

NSInteger curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue] << 16;
    [UIView animateWithDuration:animDuration delay:0.0 options:curve animations:^{

@TimMedcalf
Copy link
Author

Hey Lukas - good point about the string constants! Not sure what I was thinking there. And that's a great tip regarding the animation curve - I'll update it soon, but I feel there's room for further improvements as with iOS8 due to the autotype/spelling/suggestion bar at the top of the keyboard. That can be hidden without hiding the whole keyboard so I'll need to be monitoring the keyboard frame (i think) instead of the hide/show events.

@Prashantpukale
Copy link

Looks good, But how does this works for undocked/split keyboard?? the keyboard notifications here are applicable only for docked keyboard.

@kcbarry
Copy link

kcbarry commented May 7, 2017

Hi Tim,

Not sure if you're still maintaining this gist but I have a couple comments:
I think it's more robust if you record the content inset you've applied to the bottom. You would encounter an issue if something else was also manipulating the contentInset of the scrollView i.e. when UIViewController.automaticallyAdjustsScrollViewInsets == true. This is typically used when a view controller is contained by a UINavigationController. Setting contentInset to UIEdgeInsetsZero will stick content under the navigation bar.

I also think it's probably better to unregister for the notifications in viewDidDisappear, instead of viewWillDisappear. The scenario I'm imagining is that if the view controller has an interactive transition and it's cancelled. -[UIViewController viewWillDisappear] will be called when the transition begins, but I don't think -[UIViewController viewWillAppear] will be called again because the view has already appeared. Thus, cancelling an interactive transition will break subsequent keyboard presentations.

@d0z0
Copy link

d0z0 commented May 8, 2018

This gist worked perfectly, but I had issue in iOS11 around the safe area.
To get it to work i had to make some minor modifications..

- (void)keyboardWillShow:(NSNotification *)notification {

    // ... calculate keyboardFrame and convert it

    CGRect containerFrame;
    if (@available(iOS 11.0, *)) {
        containerFrame = self.view.safeAreaLayoutGuide.layoutFrame;
    } else {
        // Fallback on earlier versions
        containerFrame = self.myTableView.bounds;
    }
    CGRect intersect = CGRectIntersection(keyboardFrame, containerFrame);

    // ... use intersect

}
- (void)keyboardWillHide:(NSNotification *) notification{
    NSDictionary *keyInfo = [notification userInfo];
    NSTimeInterval duration = [[keyInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    NSInteger curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue] << 16;
    //change the table insets to match - animated to the same duration of the keyboard appearance
    [UIView animateWithDuration:duration delay:0.0 options:curve animations: ^{
        if (@available(iOS 11.0, *)) {
            self.myTableView.contentInset = self.tableView.adjustedContentInset;
            self.myTableView.scrollIndicatorInsets = self.tableView.adjustedContentInset;
        } else {
            // Fallback on earlier versions
            self.myTableView.contentInset = UIEdgeInsetsZero;
            self.myTableView.scrollIndicatorInsets = UIEdgeInsetsZero;
        }
    } completion:nil];
}

@wemgl
Copy link

wemgl commented Apr 16, 2020

This is working for me as of iOS 13 & Xcode 11.4

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification,
                                               object: nil, queue: OperationQueue.main,
                                               using: keyboardWillShowNotification)
        
        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification,
                                               object: nil, queue: OperationQueue.main,
                                               using: keyboardWillHideNotification)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
    }
    
    func keyboardWillShowNotification(notification: Notification) {
        let userInfo = notification.userInfo!
        let keyboardSize = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
        let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardSize.height, right: 0.0)
        self.tableView.contentInset = contentInsets
        self.tableView.scrollIndicatorInsets = contentInsets
        
        var rect = self.view.frame
        rect.size.height -= keyboardSize.height
        if let activeTextField = activeTextField, !rect.contains(activeTextField.frame.origin) {
            self.tableView.scrollRectToVisible(activeTextField.frame, animated: true)
        }
    }
    
    func keyboardWillHideNotification(notification: Notification) {
        let contentInsets = UIEdgeInsets.zero
        self.tableView.contentInset = contentInsets
        self.tableView.scrollIndicatorInsets = contentInsets
    }

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