Scroll attachment description views to ensure caret is always visible
This commit is contained in:
parent
80cca7673a
commit
366378f267
|
@ -214,6 +214,7 @@
|
|||
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */; };
|
||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; };
|
||||
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */; };
|
||||
D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */; };
|
||||
D6A5BB2B23BAEF61003BF21D /* APIMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */; };
|
||||
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
||||
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
||||
|
@ -562,6 +563,7 @@
|
|||
D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherViewController.swift; sourceTree = "<group>"; };
|
||||
D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FastAccountSwitcherViewController.xib; sourceTree = "<group>"; };
|
||||
D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastSwitchingAccountView.swift; sourceTree = "<group>"; };
|
||||
D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTextViewCaretScrolling.swift; sourceTree = "<group>"; };
|
||||
D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIMocks.swift; sourceTree = "<group>"; };
|
||||
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
|
||||
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
|
||||
|
@ -1027,6 +1029,7 @@
|
|||
D677284724ECBCB100C732D3 /* ComposeView.swift */,
|
||||
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */,
|
||||
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */,
|
||||
D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */,
|
||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
||||
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
||||
|
@ -1861,6 +1864,7 @@
|
|||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */,
|
||||
D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */,
|
||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
|
||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||
|
|
|
@ -112,6 +112,7 @@ struct ComposeAttachmentsList: View {
|
|||
let proxy = UITableView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
||||
// enable drag and drop to reorder on iPhone
|
||||
proxy.dragInteractionEnabled = true
|
||||
proxy.isScrollEnabled = false
|
||||
}
|
||||
|
||||
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
||||
|
|
|
@ -101,9 +101,10 @@ struct WrappedTextView: UIViewRepresentable {
|
|||
return Coordinator(text: $text, didChange: textDidChange)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextViewDelegate {
|
||||
class Coordinator: NSObject, UITextViewDelegate, ComposeTextViewCaretScrolling {
|
||||
var text: Binding<String>
|
||||
var didChange: ((UITextView) -> Void)?
|
||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||
|
||||
init(text: Binding<String>, didChange: ((UITextView) -> Void)?) {
|
||||
self.text = text
|
||||
|
@ -113,6 +114,8 @@ struct WrappedTextView: UIViewRepresentable {
|
|||
func textViewDidChange(_ textView: UITextView) {
|
||||
text.wrappedValue = textView.text
|
||||
didChange?(textView)
|
||||
|
||||
ensureCursorVisible(textView: textView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// ComposeTextViewCaretScrolling.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/11/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol ComposeTextViewCaretScrolling: class {
|
||||
var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set }
|
||||
}
|
||||
|
||||
extension ComposeTextViewCaretScrolling {
|
||||
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.1, curve: .linear) {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -171,12 +171,12 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
return Coordinator(text: $text, uiState: uiState, didChange: textDidChange)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler {
|
||||
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler, ComposeTextViewCaretScrolling {
|
||||
weak var textView: UITextView?
|
||||
var text: Binding<String>
|
||||
var didChange: (UITextView) -> Void
|
||||
var uiState: ComposeUIState
|
||||
private var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||
|
||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||
self.text = text
|
||||
|
@ -188,54 +188,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
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)
|
||||
|
||||
// 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.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
|
||||
ensureCursorVisible(textView: textView)
|
||||
}
|
||||
|
||||
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
||||
|
|
Loading…
Reference in New Issue