I had the same issue, but about UITextView within UITableView, so after some investigation I didn't find any "easy" way to fix it, so based on accepted answer I'd created perfectly working solution (should work also inside UICollectionView, UIScrollView with some changes commented inside this extension).
So for easy re-using it's needed some extensions on top of UIKit:
extension UITextView {
func scrollToCursor(animated: Bool = false, verticalInset: CGFloat = 8) {
guard let selectedTextRange = selectedTextRange else { return }
var cursorRect = caretRect(for: selectedTextRange.start)
// NOTE: can't point UIScrollView, coz on iOS 10 closest view will be UITableWrapperView
// to extend functionality for UICollectionView or plain UIScrollView it's better to search them one by one
let scrollView = findParent(of: UITableView.self) ?? self
cursorRect = convert(cursorRect, to: scrollView)
if cursorRect.origin.x.isInfinite || cursorRect.origin.y.isInfinite {
return
}
let bottomOverflow = cursorRect.maxY - (scrollView.contentOffset.y + scrollView.bounds.height - scrollView.contentInset.bottom - scrollView.contentInset.top)
if bottomOverflow > 0 {
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + bottomOverflow + verticalInset)
scrollView.setContentOffset(offset, animated: animated)
return
}
let topOverflow = scrollView.contentOffset.y - cursorRect.minY
if topOverflow > 0 {
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y - topOverflow - verticalInset)
scrollView.setContentOffset(offset, animated: animated)
}
}
}
UIView:
extension UIView {
func findParent<Parent: UIView>(of parentType: Parent.Type) -> Parent? {
return superview?.findNext(of: parentType)
}
private func findNext<Parent: UIView>(of parentType: Parent.Type) -> Parent? {
if let res = self as? Parent {
return res
}
return superview?.findNext(of: parentType)
}
}
So on UITextViewDelegate, when text is changed, call where you need (may be inside dispatch queue main async block - I'm using ReactiveSwift callback for this):
textView.scrollToCursor()
If you wanna add moving up on cursor position change (on top part of the screen) need to call this method inside textViewDidChangeSelection
delegate's method (with check on selection length of course).