Tusker/Packages/ComposeUI/Sources/ComposeUI/TextViewCaretScrolling.swift

62 lines
2.3 KiB
Swift
Raw Normal View History

//
// 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)
scrollView.layoutIfNeeded()
}
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
}
}