SwiftUI中UIViewRepresentable包装UITextView高度自适应问题
SwiftUI VStack内嵌UIViewRepresentable封装UITextView高度自适应方案
问题核心
- 需求:多段HTML转换的富文本通过自定义UITextView(UIViewRepresentable封装)垂直排列在ScrollView嵌套的VStack中,要求文本完整展开、组件自身不出现内部滚动
- 无效尝试:
scaledToFit、maxHeight: .infinity、fixedSize等修饰符均无法实现预期效果
实现方案
问题根源是UIKit端的UITextView不会自动向SwiftUI布局系统同步自身内容高度,需要手动打通高度计算、回传、绑定的链路,同时关闭UITextView自身的滚动能力。
1. 自定义富文本TextView封装代码
关键配置点:
- 强制关闭UITextView滚动能力
- 移除默认文本容器内边距避免高度计算偏差
- 监听内容尺寸变化,实时将计算后的内容高度回传给SwiftUI
import SwiftUI struct HTMLTextView: UIViewRepresentable { let htmlAttributedText: NSAttributedString @Binding var contentHeight: CGFloat func makeUIView(context: Context) -> UITextView { let textView = UITextView() // 基础配置 textView.isEditable = false textView.isScrollEnabled = false textView.showsVerticalScrollIndicator = false textView.showsHorizontalScrollIndicator = false textView.backgroundColor = .clear // 移除默认边距,可根据自身UI需求调整 textView.textContainer.lineFragmentPadding = 0 textView.textContainerInset = .zero // 监听内容尺寸变化 textView.addObserver( context.coordinator, forKeyPath: "contentSize", options: .new, context: nil ) return textView } func updateUIView(_ uiView: UITextView, context: Context) { uiView.attributedText = htmlAttributedText // 主线程异步更新高度,避免布局警告 DispatchQueue.main.async { let calculatedHeight = uiView.sizeThatFits( CGSize(width: uiView.frame.width, height: .greatestFiniteMagnitude) ).height contentHeight = calculatedHeight } } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject { private let parent: HTMLTextView init(parent: HTMLTextView) { self.parent = parent } override func observeValue( forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer? ) { guard keyPath == "contentSize", let textView = object as? UITextView else { return } let calculatedHeight = textView.sizeThatFits( CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude) ).height DispatchQueue.main.async { self.parent.contentHeight = calculatedHeight } } } }
2. 页面层调用示例
为每个文本组件单独维护高度状态,直接将回传的高度绑定为组件的frame高度即可,不需要额外添加之前尝试的无效修饰符:
struct PageContentView: View { // 替换为自身业务的html转富文本结果数组 private let richTextArray: [NSAttributedString] // 存储每个文本视图的高度,初始值可设为任意非零值 @State private var textViewHeights: [CGFloat] init(richTextArray: [NSAttributedString]) { self.richTextArray = richTextArray self._textViewHeights = State(initialValue: Array(repeating: 50, count: richTextArray.count)) } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { ForEach(richTextArray.indices, id: \.self) { index in HTMLTextView( htmlAttributedText: richTextArray[index], contentHeight: $textViewHeights[index] ) .frame(height: textViewHeights[index]) } } .padding(.horizontal, 16) } } }
注意事项
- 如果HTML内容包含异步加载的图片、自定义字体等资源,需要在资源加载完成后手动触发一次contentSize监听回调,避免高度计算不足导致内容截断
- 不要给封装的文本组件设置
maxHeight: .infinity这类约束,会干扰SwiftUI布局逻辑 - 如果需要支持文本编辑,仅需将
isEditable属性改为true,组件会随输入内容自动更新高度
内容的提问来源于stack exchange,提问作者danifm1321
相关产品推荐
相关产品推荐

