SwiftUI UITextView 布局问题: 禁用滚动解决方案
2025-02-06 00:49:39
UIViewRepresentable 中 UITextView 的布局问题:禁用滚动时的解决方案
在 SwiftUI 中使用 UIViewRepresentable
封装 UITextView
时,如果禁用滚动(isScrollEnabled = false
),可能会遇到布局失效的问题。 具体表现为, UITextView
无法正确地根据内容调整自身大小,导致显示异常甚至空白。这与 UITextView
的内部布局机制以及 SwiftUI 的布局方式有关。
问题分析:intrinsicContentSize失效的原因
当 isScrollEnabled
为 true 时,UITextView
会根据其 contentSize
来确定其自身大小。 但是当 isScrollEnabled
设置为 false 时,UITextView
不再滚动,它的内容会受到自身 frame
的约束。 这导致 intrinsicContentSize
的计算结果与预期不符,或者失效。 因为没有开启滚动条, 没有内容尺寸需要适配了,自身尺寸可能变为零, 导致外部 ScrollView
也无法正确渲染。
解决方案一: 强制更新 intrinsicContentSize
一种方案是在 updateUIView
中强制触发 UITextView
重新计算 intrinsicContentSize
。 可以通过修改其 text
或其他属性来达到这个目的。 由于SwiftUI的生命周期原因,最好放到主线程异步执行。
struct SimpleTextView: UIViewRepresentable {
@State private var attributedText: NSAttributedString?
var text: String
func makeUIView(context: Context) -> UITextView {
let view = ContentTextView()
view.setContentHuggingPriority(.required, for: .vertical)
view.setContentHuggingPriority(.required, for: .horizontal)
view.isSelectable = true
view.isEditable = false
view.isScrollEnabled = false // 禁用滚动
view.isUserInteractionEnabled = true
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
guard let attributedText = attributedText else {
generateAttributedText()
return
}
uiView.attributedText = attributedText
// 强制更新 intrinsicContentSize
DispatchQueue.main.async {
uiView.invalidateIntrinsicContentSize()
}
}
private func generateAttributedText() {
guard attributedText == nil else { return }
DispatchQueue.main.async {
self.attributedText = NSAttributedString(string: text, attributes: [:])
}
}
private class ContentTextView: UITextView {
override var canBecomeFirstResponder: Bool { false }
override var intrinsicContentSize: CGSize {
frame.height > 0 ? contentSize : super.intrinsicContentSize
}
}
}
操作步骤:
- 将
UITextView
的isScrollEnabled
属性设置为false
。 - 在
updateUIView
方法中使用DispatchQueue.main.async
包装uiView.invalidateIntrinsicContentSize()
,保证在主线程异步执行。
原理: invalidateIntrinsicContentSize()
会通知 UITextView
其内部状态已发生变化,需要重新计算 intrinsicContentSize
。通过在主线程异步调用,避免阻塞UI线程,并且可以等待UI渲染完毕后再进行重新计算。
解决方案二: 手动计算内容高度并设置Frame
若强制更新 intrinsicContentSize 无效,可选择手动计算 UITextView
的内容高度,并设置其 frame
。 这涉及到使用 sizeThatFits(_:)
方法来测量文本内容的尺寸。
该方法用于根据给定的尺寸约束来计算文本所需的最佳尺寸。
struct SimpleTextView: UIViewRepresentable {
@State private var attributedText: NSAttributedString?
var text: String
func makeUIView(context: Context) -> UITextView {
let view = ContentTextView()
view.setContentHuggingPriority(.required, for: .vertical)
view.setContentHuggingPriority(.required, for: .horizontal)
view.isSelectable = true
view.isEditable = false
view.isScrollEnabled = false // 禁用滚动
view.isUserInteractionEnabled = true
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
guard let attributedText = attributedText else {
generateAttributedText()
return
}
uiView.attributedText = attributedText
// 手动计算内容高度
let fixedWidth = uiView.frame.size.width
let newSize = uiView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
uiView.frame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height) // Fix auto layout error
// 可以不需要手动 invalidate 了,因为 frame 已经改变
//DispatchQueue.main.async {
// uiView.invalidateIntrinsicContentSize()
//}
}
private func generateAttributedText() {
guard attributedText == nil else { return }
DispatchQueue.main.async {
self.attributedText = NSAttributedString(string: text, attributes: [:])
}
}
private class ContentTextView: UITextView {
override var canBecomeFirstResponder: Bool { false }
override var intrinsicContentSize: CGSize {
//frame.height > 0 ? contentSize : super.intrinsicContentSize // 注释掉这里,让系统自动计算
return contentSize
}
}
}
操作步骤:
- 在
updateUIView
方法中,首先获取UITextView
的当前宽度。 - 使用
sizeThatFits(_:)
方法计算文本内容在给定宽度下的所需高度。 需要提供一个高度上限,例如CGFloat.greatestFiniteMagnitude
表示不限制高度。 - 设置
UITextView
的frame
为计算出的尺寸。确保宽度不小于初始宽度,以防止产生 Auto Layout 的警告或错误。 - ContentTextView 不需要覆写 intrinsicContentSize , 直接使用父类的
contentSize
。
原理: sizeThatFits(_:)
方法会根据文本的属性(字体、字号等)以及给定的宽度约束来计算文本实际需要的尺寸。通过手动设置 frame
,可以强制 UITextView
按照计算结果进行布局,绕过 isScrollEnabled = false
带来的限制。
安全建议
- 在更新
UITextView
的frame
之后,需要仔细测试布局效果,确保在不同设备和屏幕尺寸下都能正确显示。 - 尽可能避免频繁地更新
frame
,这可能会影响性能。 考虑使用缓存或其他优化策略来减少布局计算的次数。 - 注意
UITextView
的其他属性,例如textContainerInset
,contentInset
,这些属性也可能影响布局结果, 需要根据实际情况进行调整。 - 检查 constraints (约束),可能约束产生了影响。