在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
相关产品推荐
相关产品推荐

