Last active
May 6, 2024 14:21
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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]; | |
} |
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
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..