Make a UITableView "sticky to the bottom"

Viewed 1018

Say you have a table view T which shows messages,

T[ message d
T[ message c
T[ message b
T[ most recent message down bottom

Say there are 100 messages, with the bottom 4 visible in the example.

So the table view height is 700 say. You have a typical text entry underneath...

T[ message d
T[ message c
T[ message b
T[ most recent message down bottom
[enter snapped chat message!] [send]

When the keyboard appears, the new height of the visible table view is 400 say.

Of course, that will "cut off the bottom" of the messages - the most recent two will no longer be visible.

T[ message d
T[ message c
[enter snapped chat message!] [send]
[           iOS keyboard        ]
[           iOS keyboard        ]

(So, messages A and B are now "under" the keyboard.)

Naturally what you do is just scroll the table after the keyboard appears, for example. No problem doing it in an ad-hoc manner.

However, it would be really natural if one could subclass UITableView in such a way that, as the size of the visible area changed, the table view knew to keep the "bottom point" identical.

So, as the bottom of the table moves up and down (due to keyboard appearing - or whatever cause), the table would scroll actually based on the movement of the "base" of the table.

(Apart from anything else this would solve the "match the animation timing" nuisance.)

Could this be achieved elegantly and if so how - indeed this would seem so natural these days, perhaps it's built-in to UITableView as a flag and I just don't know?

once again the question here is

How to modify UITableView so that it moves its own scroll position, as, the view size changes...

(so as to keep the "bottom point the same")

Note that it's trivial to just scroll the table "manually" as it were from the outside.

3 Answers

This is a way late answer, but I just solved the problem a bit more elegantly than the very ingenious accepted solution.

You'll need to have the following AutoLayout constraints (I'll omit the horizontal constraints):

  • UITableView.top = Safe Area.top
  • UITableView.bottom = MessageContainerView.top
  • MessageContainerView.bottom = Safe Area.bottom

MessageContainerView is where I keep the message text view and button. Now, in your ViewController add an IBOutlet for the bottom constraint of MessageContainerView:

@IBOutlet weak var messageContainerBottomConstraint: NSLayoutConstraint!

Next step is animating the bottom constraint's constant to be equal to the keyboard height + the safe area at the bottom and scrolling the table to the bottom when the keyboard is shown (add an observer for the notification as well):

@objc func showKeyboard(_ notification: NSNotification) {
  guard let userInfo = notification.userInfo,
    let targetFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
    let animationTime = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
    else {
      return
  }

  let safeAreaBottom = view.safeAreaInsets.bottom
  messageContainerBottomConstraint.constant = -targetFrame.cgRectValue.height + safeAreaBottom
  UIView.animate(withDuration: animationTime) {
    self.view.layoutIfNeeded()
    self.tableView.scrollToBottom()
  }
}

Note that we use UIResponder.keyboardAnimationDurationUserInfoKey for the animation timing, so everything will be smooth.

Lastly, tableView.scrollToBottom() is an Extension for UITableView:

import UIKit

extension UITableView {
  func scrollToBottom() {
    let numberOfSections = self.numberOfSections
    let numberOfRowsInSection = self.numberOfRows(inSection: numberOfSections - 1)
    let indexPathForLastRow = IndexPath(row: numberOfRowsInSection - 1, section: numberOfSections - 1)
    self.scrollToRow(at: indexPathForLastRow, at: .bottom, animated: false)
  }
}

Let me know if you can try it, and if I missed anything.

(I did notice you asked for a way to modify the native UITableView behavior instead of "manually" scrolling, but I think this is a valid way of solving the problem)

Related