From 16b02edf871c547d1391ae7f923c67e6519368bf Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 24 Oct 2020 15:46:24 -0400 Subject: [PATCH] Ensure the cursor remains visible when composing posts --- .../Screens/Compose/MainComposeTextView.swift | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Tusker/Screens/Compose/MainComposeTextView.swift b/Tusker/Screens/Compose/MainComposeTextView.swift index 2ac83120..3092b7c8 100644 --- a/Tusker/Screens/Compose/MainComposeTextView.swift +++ b/Tusker/Screens/Compose/MainComposeTextView.swift @@ -176,6 +176,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable { var text: Binding var didChange: (UITextView) -> Void var uiState: ComposeUIState + private var caretScrollPositionAnimator: UIViewPropertyAnimator? init(text: Binding, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) { self.text = text @@ -186,6 +187,54 @@ struct MainComposeWrappedTextView: UIViewRepresentable { func textViewDidChange(_ textView: UITextView) { text.wrappedValue = textView.text didChange(textView) + + ensureCursorVisible() + } + + private func ensureCursorVisible() { + guard let textView = textView, + textView.isFirstResponder, + let range = textView.selectedTextRange, + let scrollView = findParentScrollView() 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) + + // move Y position of the rect that will be made visible down by the cursor's height 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 + + let animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) { + scrollView.scrollRectToVisible(rectToMakeVisible, animated: false) + } + self.caretScrollPositionAnimator = animator + animator.startAnimation() + } + + private func findParentScrollView() -> UIScrollView? { + guard let textView = textView else { return nil } + + var current: UIView = textView + while let superview = current.superview { + if let scrollView = superview as? UIScrollView { + return scrollView + } else { + current = superview + } + } + + return nil } @objc func formatButtonPressed(_ sender: UIBarButtonItem) {