You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 11:31:07