-
-
Save TimMedcalf/9505416 to your computer and use it in GitHub Desktop.
/* | |
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]; | |
} |
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:^{
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.
Looks good, But how does this works for undocked/split keyboard?? the keyboard notifications here are applicable only for docked keyboard.
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.
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];
}
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
}
IOS7 is OK, but in IOS6, nothing happens.