forked from shadowfacts/Tusker
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 */; };
|
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */; };
|
||||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; };
|
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; };
|
||||||
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */; };
|
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 */; };
|
D6A5BB2B23BAEF61003BF21D /* APIMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2A23BAEF61003BF21D /* APIMocks.swift */; };
|
||||||
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
||||||
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1027,6 +1029,7 @@
|
||||||
D677284724ECBCB100C732D3 /* ComposeView.swift */,
|
D677284724ECBCB100C732D3 /* ComposeView.swift */,
|
||||||
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */,
|
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */,
|
||||||
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */,
|
D677284B24ECBE9100C732D3 /* ComposeAvatarImageView.swift */,
|
||||||
|
D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */,
|
||||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
||||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
||||||
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
||||||
|
@ -1861,6 +1864,7 @@
|
||||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||||
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */,
|
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */,
|
||||||
|
D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */,
|
||||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
|
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
|
||||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||||
|
|
|
@ -112,6 +112,7 @@ struct ComposeAttachmentsList: View {
|
||||||
let proxy = UITableView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
let proxy = UITableView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
||||||
// enable drag and drop to reorder on iPhone
|
// enable drag and drop to reorder on iPhone
|
||||||
proxy.dragInteractionEnabled = true
|
proxy.dragInteractionEnabled = true
|
||||||
|
proxy.isScrollEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
||||||
|
|
|
@ -101,9 +101,10 @@ struct WrappedTextView: UIViewRepresentable {
|
||||||
return Coordinator(text: $text, didChange: textDidChange)
|
return Coordinator(text: $text, didChange: textDidChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextViewDelegate {
|
class Coordinator: NSObject, UITextViewDelegate, ComposeTextViewCaretScrolling {
|
||||||
var text: Binding<String>
|
var text: Binding<String>
|
||||||
var didChange: ((UITextView) -> Void)?
|
var didChange: ((UITextView) -> Void)?
|
||||||
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
init(text: Binding<String>, didChange: ((UITextView) -> Void)?) {
|
init(text: Binding<String>, didChange: ((UITextView) -> Void)?) {
|
||||||
self.text = text
|
self.text = text
|
||||||
|
@ -113,6 +114,8 @@ struct WrappedTextView: UIViewRepresentable {
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
text.wrappedValue = textView.text
|
text.wrappedValue = textView.text
|
||||||
didChange?(textView)
|
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)
|
return Coordinator(text: $text, uiState: uiState, didChange: textDidChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler {
|
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler, ComposeTextViewCaretScrolling {
|
||||||
weak var textView: UITextView?
|
weak var textView: UITextView?
|
||||||
var text: Binding<String>
|
var text: Binding<String>
|
||||||
var didChange: (UITextView) -> Void
|
var didChange: (UITextView) -> Void
|
||||||
var uiState: ComposeUIState
|
var uiState: ComposeUIState
|
||||||
private var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||||
self.text = text
|
self.text = text
|
||||||
|
@ -188,54 +188,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
text.wrappedValue = textView.text
|
text.wrappedValue = textView.text
|
||||||
didChange(textView)
|
didChange(textView)
|
||||||
|
|
||||||
ensureCursorVisible()
|
ensureCursorVisible(textView: textView)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
||||||
|
|
Loading…
Reference in New Issue