ios - UITextView 放置在 UIScrollView 中时如何实现响应式自动滚动?
问题描述
UITextView
本身带有默认的Scrolling Enabled行为。
当在 中创建新行(按 ENTER 键)时UITextView
,将自动滚动。
如果你仔细观察,有顶部内边距和底部内边距,它们会沿着滚动方向移动。它是使用以下代码实现的。
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var bodyTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// We want to have "clipToPadding" for top and bottom.
// To understand what is "clipToPadding", please refer to
// https://stackoverflow.com/a/46710968/72437
bodyTextView.textContainerInset = UIEdgeInsets(top: 40, left: 0, bottom: 40, right: 0)
}
}
现在,在单个视图控制器中,除此之外UITextView
,还有其他 UI 组件,如标签、堆栈视图……在同一个控制器中
我们希望它们在滚动期间一起移动。因此,这是我们的改进。
- 放在
UITextView
一个UIScrollView
. - 禁用滚动启用。
UITextView
这是我们的故事板的样子
这是最初的结果
我们可以观察到以下缺点。
- 仅当我们在新行中键入第一个字符时才会发生自动滚动。当我们输入新行时,它不会立即发生。
- 自动滚动不再考虑底部填充。自动滚动仅在“触摸”屏幕底部边缘时发生。要理解这一点,请使用不带
UIScrollView
.
你有什么建议,我该如何克服这两个缺点?
可以从https://github.com/yccheok/uitextview-inside-uiscrollview下载演示此类缺点的示例项目
谢谢。
解决方案
这是一个众所周知的(并且长期运行的)怪癖UITextView
。
当输入一个换行符时,文本视图不会更新它的内容大小(以及它的框架,when .isScrollEnabled = false
),直到在新行上输入另一个字符。
似乎大多数人刚刚接受了它作为苹果的默认行为。
您想要进行彻底的测试,但经过一些快速测试后,这似乎是可靠的:
func textViewDidChange(_ textView: UITextView) {
// if the cursor is at the end of the text, and the last char is a newline
if let selectedRange = textView.selectedTextRange,
let txt = textView.text,
!txt.isEmpty,
txt.last == "\n" {
let cursorPosition = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
if cursorPosition == txt.count {
// UITextView has a quirk when last char is a newline...
// its size is not updated until another char is entered
// so, this will force the textView to scroll up
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
self.textView.sizeToFit()
self.textView.layoutIfNeeded()
// might prefer setting animated: true
self.scrollView.scrollRectToVisible(self.textView.frame, animated: false)
})
}
}
}
这是一个完整的示例实现:
class ViewController: UIViewController, UITextViewDelegate {
let textView: UITextView = {
let v = UITextView()
v.textColor = .black
v.backgroundColor = .green
v.font = .systemFont(ofSize: 17.0)
v.isScrollEnabled = false
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .red
v.alwaysBounceVertical = true
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// create a vertical stack view
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 8
// add a few labels to the stack view
let strs: [String] = [
"Three Labels in a Vertical Stack View",
"Just so we can see that there are UI elements in the scroll view in addition to the text view.",
"Stack View is, of course,\nusing auto-layout constraints."
]
strs.forEach { str in
let v = UILabel()
v.backgroundColor = .yellow
v.text = str
v.textAlignment = .center
v.numberOfLines = 0
stackView.addArrangedSubview(v)
}
// we're setting constraints
[scrollView, stackView, textView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add views to hierarchy
scrollView.addSubview(stackView)
scrollView.addSubview(textView)
view.addSubview(scrollView)
// respect safe area
let g = view.safeAreaLayoutGuide
// references to scroll view's Content and Frame Layout Guides
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView to view (safe area)
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constrain stackView Top / Leading / Trailing to Content Layout Guide
stackView.topAnchor.constraint(equalTo: cg.topAnchor),
stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
// stackView width equals scrollView Frame Layout Guide width
stackView.widthAnchor.constraint(equalTo: fg.widthAnchor),
// constrain textView Top to stackView Bottom + 12
// Leading / Trailing to Content Layout Guide
textView.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 12.0),
textView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
// textView width equals scrollView Frame Layout Guide width
textView.widthAnchor.constraint(equalTo: fg.widthAnchor),
// constrain textView Bottom to Content Layout Guide
textView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
])
// some initial text
textView.text = "This is the textView"
// set the delegate
textView.delegate = self
// if we want
//textView.textContainerInset = UIEdgeInsets(top: 40, left: 0, bottom: 40, right: 0)
// a right-bar-button to end editing
let btn = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done(_:)))
navigationItem.setRightBarButton(btn, animated: true)
// keyboard notifications
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
@objc func done(_ b: Any?) -> Void {
view.endEditing(true)
}
@objc func adjustForKeyboard(notification: Notification) {
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardScreenEndFrame = keyboardValue.cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
if notification.name == UIResponder.keyboardWillHideNotification {
scrollView.contentInset = .zero
} else {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0)
}
scrollView.scrollIndicatorInsets = scrollView.contentInset
}
func textViewDidChange(_ textView: UITextView) {
// if the cursor is at the end of the text, and the last char is a newline
if let selectedRange = textView.selectedTextRange,
let txt = textView.text,
!txt.isEmpty,
txt.last == "\n" {
let cursorPosition = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
if cursorPosition == txt.count {
// UITextView has a quirk when last char is a newline...
// its size is not updated until another char is entered
// so, this will force the textView to scroll up
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
self.textView.sizeToFit()
self.textView.layoutIfNeeded()
// might prefer setting animated: true
self.scrollView.scrollRectToVisible(self.textView.frame, animated: false)
})
}
}
}
}
推荐阅读
- javascript - 如何使用 react-p5-wrapper 将 p5.sound 添加到 React?
- python - 将过滤后的列表值传递给数据框
- java - 从数组中删除元素。获取 null 而不是实际删除元素/索引
- android - 将 URL 缩短为自定义 URI 方案深层链接
- python - 如何将实时数据从一个 Python 脚本传输到另一个?
- javascript - 如何从JS中的数组中删除特定元素
- php - PHP 会话:$_SESSION 在 Mac 上本地运行时为空
- directed-acyclic-graphs - How can I encode a rule tree of multiple depths in Dhall?
- c# - 通过 id 获取时,EF Core 和 Automapper 未获取所有数据
- r - 计算 R 中每组的平均、最大、最小观察数