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

在SwiftUI View的body中实例化HTML样式NSAttributedString引发异常

SwiftUI HTML文本渲染问题解析与优化

核心疑问解答

1. 为什么AttributeGraph会检测到循环?

你在HTML的body中直接执行HTML转NSAttributedString的操作,这个过程会触发UIKit的布局计算。SwiftUI的AttributeGraph在追踪视图依赖时,会因为跨框架的布局触发反向依赖更新,形成循环:

  • SwiftUI计算HTML.body时,生成NSAttributedString会间接触发UITextView的intrinsicContentSize计算
  • 尺寸变化反馈给SwiftUI布局系统,导致HTML.body被重新计算,往复形成循环,最终被AttributeGraph检测到。

2. 为什么传入新bodyText后HTML View不更新?

HTML结构体中的bodyText是私有常量,SwiftUI的视图更新依赖@State/@Binding等可追踪的状态变量,普通常量无法被SwiftUI感知变化,自然不会重新计算HTML的body。

优化方案(保留HTML View抽象性)

我们需要把HTML转NSAttributedString的耗时操作移出body,用缓存机制存储结果,同时让bodyText成为可追踪的输入:

优化后的HTML View代码

import SwiftUI

struct HTML: View {
    let bodyText: String
    // 缓存转换后的富文本,仅在bodyText变化时重新计算
    @State private var attributedString: NSAttributedString?

    init(_ bodyText: String) {
        self.bodyText = bodyText
        // 初始化时预计算一次
        _attributedString = State(initialValue: convertToAttributedString(bodyText))
    }

    var body: some View {
        AttributedText(attributedString ?? NSAttributedString(string: bodyText))
            .fixedSize(horizontal: false, vertical: true)
            .onChange(of: bodyText) { newValue in
                // bodyText变化时更新缓存
                attributedString = convertToAttributedString(newValue)
            }
    }

    // 抽离转换逻辑,避免在body中执行耗时操作
    private func convertToAttributedString(_ text: String) -> NSAttributedString? {
        let htmlTemplate = """
        <!doctype html>
        <html>
        <head>
            <meta charset="utf-8">
            <style type="text/css">
                body {
                    font: -apple-system-body;
                    color: white;
                }
            </style>
        </head>
        <body>
            \(text)
        </body>
        </html>
        """
        guard let data = htmlTemplate.data(using: .utf8) else { return nil }
        return try? NSAttributedString(
            data: data,
            options: [
                .documentType: NSAttributedString.DocumentType.html,
                .characterEncoding: NSUTF8StringEncoding,
            ],
            documentAttributes: nil
        )
    }
}

优化后的AttributedText

调整布局优先级,避免不必要的布局触发:

import SwiftUI

struct AttributedText: UIViewRepresentable {
    private let attributedString: NSAttributedString

    init(_ attributedString: NSAttributedString) {
        self.attributedString = attributedString
    }

    func makeUIView(context: Context) -> UITextView {
        let uiTextView = UITextView()

        uiTextView.backgroundColor = .clear
        uiTextView.isEditable = false
        uiTextView.isScrollEnabled = false

        // 调整优先级,让SwiftUI更好控制布局
        uiTextView.setContentHuggingPriority(.required, for: .vertical)
        uiTextView.setContentCompressionResistancePriority(.required, for: .vertical)
        uiTextView.setContentHuggingPriority(.defaultLow, for: .horizontal)
        uiTextView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

        return uiTextView
    }

    func updateUIView(_ uiTextView: UITextView, context: Context) {
        // 仅内容变化时更新,避免不必要的布局波动
        if uiTextView.attributedText != attributedString {
            uiTextView.attributedText = attributedString
        }
    }
}

测试验证代码

添加状态变量测试文本更新功能:

import SwiftUI

struct ContentView: View {
    @State private var htmlText = """
    <p>This is a paragraph</p>
    <ul>
        <li>List item one</li>
        <li>List item two</li>
    </ul>
    """

    var body: some View {
        NavigationStack {
            ScrollView {
                HTML(htmlText)
                Button("Change HTML") {
                    htmlText = "<h2>Updated Content</h2><p>This is new text</p>"
                }
                .foregroundColor(.blue)
                .padding()
            }
            .navigationTitle("HTML in SwiftUI")
        }
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

内容的提问来源于stack exchange,提问作者thomsmed

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 22:22:48