7

I'm having difficulties resizing a tableview, and scrolling to the active textfield, when the keyboard appears, when the view is presented modally in a form sheet on an iPad. It works fine on iPhone, since I don't have to take the offset of the form sheet view into account - I can just change the bottom contentInset of the tableview to be the same as the keyboard height. This doesn't work on iPad, however, since the form sheet, and thus its tableview, doesn't occupy the entire screen.

What's the best way to calculate how much the new bottom contentInset of the tableview should be?

rodskagg
  • 3,827
  • 4
  • 27
  • 46
  • Hi! did you find a solution for your problem? I'm facing the same. wanted to know if someone else already solved this. – Fmessina Oct 27 '17 at 15:08

4 Answers4

14

Okay, I stumbled on this issue myself. I created a solution that works in every situation (so not only for viewControllers presented as a form sheet). Solution is in Swift 3, so you still need to convert it to Objective-C, but that shouldn't be a problem if your read the comments carefully.

The key of the solution is to update the tableView (or scrollview) insets when the keyboard animation is finished, and the form sheet is on it's new position.

In your UIViewController subclass add:

override func viewDidLoad() {
     super.viewDidLoad()
     NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: .UIKeyboardWillChangeFrame, object: nil)
     NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: .UIKeyboardWillHide, object: nil)
}

This will add an observer in case the keyboard will show / hide. We also need to unsubscribe from these notifications, or the app will crash:

deinit {
    NotificationCenter.default.removeObserver(self)
}

And finally, the most important code:

func getTableViewInsets(keyboardHeight: CGFloat) -> UIEdgeInsets {
    // Calculate the offset of our tableView in the 
    // coordinate space of of our window
    let window = (UIApplication.shared.delegate as! AppDelegate).window!
    let tableViewFrame = tableView.superview!.convert(tableView.frame, to: window)

    // BottomInset = part of keyboard that is covering the tableView
    let bottomInset = keyboardHeight 
        - ( window.frame.height - tableViewFrame.height - tableViewFrame.origin.y )

    // Return the new insets + update this if you have custom insets
    return UIEdgeInsetsMake(
         tableView.contentInset.top, 
         tableView.contentInset.left, 
         bottomInset, 
         tableView.contentInset.right
    )
}

func keyboardWillChangeFrame(_ notification: Notification){
    guard let info = (notification as NSNotification).userInfo else {
        return
    }

    guard let animationDuration = info[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval else {
        return
    }

    // Default: keyboard will hide:
    var keyboardHeight: CGFloat = 0

    if notification.name == .UIKeyboardWillChangeFrame {
        // The keyboard will show
        guard let keyboardFrame = info[UIKeyboardFrameEndUserInfoKey] as? NSValue else {
            return
        }

        keyboardHeight = keyboardFrame.cgRectValue.height
    }

    let contentInsets = getTableViewInsets(keyboardHeight: keyboardHeight)

    UIView.animate(withDuration: animationDuration, animations: {
        self.tableView.contentInset = contentInsets
        self.tableView.scrollIndicatorInsets = contentInsets
    }, completion: {(completed: Bool) -> Void in
        // Chances are the position of our view has changed, (form sheet)
        // so we need to double check our insets
        let contentInsets = self.getTableViewInsets(keyboardHeight: keyboardHeight)
        self.tableView.contentInset = contentInsets
        self.tableView.scrollIndicatorInsets = contentInsets
    })
}
Simon Backx
  • 1,282
  • 14
  • 16
  • 2
    Thanks, for bottomInset calculation I used `let bottomInset = keyboardHeight - (window.frame.maxY - tableViewFrame.maxY)` – user1046037 Mar 12 '18 at 04:10
  • I did something very similar to this, but changed the bottom constraint of a view that contains the tableview and a button below. I want to animate the change in the constraint, but for that I need the final position of the form sheet. Is there a way to get it? – Olav Gausaker Jan 08 '20 at 14:53
  • 1
    Why are you using `UIKeyboardWillChangeFrame` rather then `UIKeyboardWillShow`? – Radek Wilczak Apr 14 '20 at 10:50
0

First of all, thanks to Simon Backx, I used his method in my real project. It works, but there are some scenarios that are not fully considered. I optimized his answer. I originally wanted to add a comment below his answer, but we don't have enough reputation to add comments. I can only create a new answer and hope the answer can help others.

  • Register observers
override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
  • Unregister observers (If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer)
deinit {
    NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
  • Modify the contentInsets of the tableview, according to the changes of the keyboard
private func calculateTableViewContentInsets(keyboardHeight: CGFloat) -> UIEdgeInsets {
    let window = (UIApplication.shared.delegate as! AppDelegate).window!
    let tableViewFrame = tableView.superview!.convert(tableView.frame, to: window)

    let bottomInset = keyboardHeight
        - (window.frame.height - tableViewFrame.height - tableViewFrame.origin.y)
        - tableView.safeAreaInsets.bottom

    var newInsets = tableView.contentInset
    newInsets.bottom = bottomInset

    return newInsets
}


@objc private func keyboardWillChangeFrame(_ sender: Notification) {
    guard let userInfo = sender.userInfo else { return }
    guard let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
    guard let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
    let keyboardHeight = keyboardFrame.height

    let contentInsets = calculateTableViewContentInsets(keyboardHeight: keyboardHeight)
    UIView.animate(withDuration: duration, animations: {
        self.tableView.contentInset = contentInsets
        self.tableView.scrollIndicatorInsets = contentInsets
    }, completion: { _ in
        let contentInsets = self.calculateTableViewContentInsets(keyboardHeight: keyboardHeight)
        self.tableView.contentInset = contentInsets
        self.tableView.scrollIndicatorInsets = contentInsets
    })
}

@objc private func keyboardWillHide(_ sender: Notification) {
    guard let userInfo = sender.userInfo else { return }
    guard let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }

    UIView.animate(withDuration: duration) {
        self.tableView.contentInset.bottom = 0
        self.tableView.scrollIndicatorInsets.bottom = 0
    }
}
-1

Why don't you resize the entire UITableView instead of setting a contentInset ?

I would modify the tableview's constraints when the keyboard is shown.

Create a reference in your .h of the actual constraint that will change when the keyboard is hidden or not. (For example, the margin between the bottom of your tableview and the bottom of the superview.)

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomMargin;

Add observer for UIKeyboardWillShowNotification

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];

and the actual method

-(void)keyboardWillShow:(NSNotification*)notification {
    [self.view layoutIfNeeded]; 

    NSDictionary* userInfo = [notification userInfo]; 
    CGRect keyboardFrameEnd;
    [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrameEnd]; //getting the frame of the keyboard
    //you can get keyboard animation's duration and curve with UIKeyboardAnimationCurveUserInfoKey and UIKeyboardAnimationDurationUserInfoKey if you want to animate it

    //calculate the new values of the constraints of your UITableView

    //for the example, the new value will be the keyboard height
    self.bottomMargin.constant = keyboardFrameEnd.size.height;
    [self.view layoutIfNeeded];
}
Sudo
  • 991
  • 9
  • 24
  • 2
    I don't think that solves my problem, which is how to calculate by how much I should change the tableview height, or the content inset. In an iPhone application, this is easy, since the keyboard appears directly above the viewcontroller with the tableview - they have the same bottom coordinate. But in my iPad application, the tableview and its viewcontroller is located in a modal form sheet, which means I have to take the number of pixels from the bottom of the window to the bottom of the form sheet into account as well. I can calculate this, I'm just wondering what the best way to do this is. – rodskagg Feb 19 '15 at 12:50
-1

I posted a link to a demo of how to do this, in this thread:

iOS 7 iPad Orientation issue, when view moves upside after keyboard appears on UITextField

It also includes a keyboardWillShow function, but which is iOS 8 friendly:

-(void)onKeyboardWillShow:(NSNotification*)notification
{
    //  The user has turned on the onscreen keyboard.
    //
    NSDictionary *info = [notification userInfo];
    NSValue *kbFrame = [info objectForKey:UIKeyboardFrameEndUserInfoKey];
    CGRect keyboardFrame = [kbFrame CGRectValue];
    keyboardFrame = [self.view convertRect:keyboardFrame fromView:nil];

    CGFloat keyboardHeight = keyboardFrame.size.height;
}

I found that with iOS 8.x, sometimes the iPad would report a keyboard height of 1024 pixels in landscape mode, if I didn't use the convertRect function.

Community
  • 1
  • 1
Mike Gledhill
  • 27,846
  • 7
  • 149
  • 159