// // TextViewCaretScrolling.swift // Tusker // // Created by Shadowfacts on 11/11/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import UIKit protocol TextViewCaretScrolling: AnyObject { var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set } } extension TextViewCaretScrolling { func ensureCursorVisible(textView: UITextView) { guard textView.isFirstResponder, let range = textView.selectedTextRange, let scrollView = findParentScrollView(of: textView) else { return } // We use a UIViewProperty animator to change the scroll view position so that we can store the currently // running one on the Coordinator. This allows us to cancel the running one, preventing multiple animations // from attempting to change the scroll view offset simultaneously, causing it to jitter around. This can // happen if the user is pressing return and quickly creating many new lines. if let existing = caretScrollPositionAnimator { existing.stopAnimation(true) } let cursorRect = textView.caretRect(for: range.start) var rectToMakeVisible = textView.convert(cursorRect, to: scrollView) // expand the rect to be three times the cursor height centered on the cursor so that there's // some space between the bottom of the line of text being edited and the top of the keyboard rectToMakeVisible.origin.y -= cursorRect.height rectToMakeVisible.size.height *= 3 let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut) { scrollView.scrollRectToVisible(rectToMakeVisible, animated: false) } self.caretScrollPositionAnimator = animator animator.startAnimation() } private func findParentScrollView(of view: UIView) -> UIScrollView? { var current: UIView = view while let superview = current.superview { if let scrollView = superview as? UIScrollView, scrollView.isScrollEnabled { return scrollView } else { current = superview } } return nil } }