2023-04-16 17:23:13 +00:00
|
|
|
//
|
|
|
|
// 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)
|
2024-07-23 05:48:00 +00:00
|
|
|
scrollView.layoutIfNeeded()
|
2023-04-16 17:23:13 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|