Skip to content

Instantly share code, notes, and snippets.

@TimMedcalf
Last active July 5, 2021 12:18
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • 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
tim@kadixi.com
*/
- (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];
}
@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