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

使用AsyncDisplayKit/Texture时,ASTextNode含NSTextAttachment图片如何重载?

Texture框架中ASTextNode更新NSTextAttachment图片不显示的问题解决

问题背景

在iOS应用中使用AsyncDisplayKit/Texture框架,通过ASTableNode展示包含NSTextAttachment图片的富文本。图片从URL异步下载(示例为简化使用Bundle内图片),获取图片后更新了NSTextAttachment的bounds以匹配实际图片宽高比,但调用setNeedsLayout()和layoutIfNeeded()后,ASTextNode仍未显示图片,需排查遗漏点。

问题原因分析

  • NSAttributedString不可变性:直接修改已赋值给ASTextNode的NSAttributedString内部的NSTextAttachment属性,不会触发节点更新——ASTextNode通过监听attributedText的引用变化来识别内容变更。
  • 同步阻塞操作:在didEnterPreloadState中使用NSData(contentsOf:)同步读取文件,会阻塞主线程,影响Texture的滑动性能,不符合框架异步设计原则。
  • 错误的更新方式:仅调用setNeedsLayout()无法让ASTextNode感知到富文本内容的变化,必须重新设置attributedText才能触发重绘。

解决方案

  • 复制原富文本为NSMutableAttributedString,修改对应位置的NSTextAttachment后,重新赋值给myTextNode.attributedText。
  • 改用异步方式加载图片,避免阻塞主线程。
  • 无需手动调用layoutIfNeeded(),更新attributedText后Texture会自动处理节点的布局和重绘。

修正后的完整代码

import UIKit
import AsyncDisplayKit

class ViewController: ASDKViewController<ASDisplayNode>, ASTableDataSource {

    let tableNode = ASTableNode()
    
    override init() {
      super.init(node: tableNode)
      tableNode.dataSource = self
    }

    required init?(coder aDecoder: NSCoder) {
      fatalError()
    }

    func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
        return 1
    }
    
    func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
        let row = indexPath.row
        return {
            let node = MyCellNode(index: row, before: """
                                  Item \(row).
                                  Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
                                  """,
            after: """
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old.
""")
            return node
        }
    }
}

class MyCellNode: ASCellNode {
    
    fileprivate var myTextNode = ASTextNode()
    // 保存原始可变富文本,方便后续修改
    private var originalAttributedText: NSMutableAttributedString?
    
    init(index : Int, before: String, after: String) {
        super.init()
        debugName = "Row \(index)"
        automaticallyManagesSubnodes = true
        automaticallyRelayoutOnSafeAreaChanges = true
        automaticallyRelayoutOnLayoutMarginsChanges = true
        
        let attributedText = NSMutableAttributedString(attributedString: (before+"\n").formattedText())
        
        let attachment = CustomAttachment(url: Bundle.main.url(forResource: "test", withExtension: "png")!)
        attachment.bounds = CGRect(x: 0, y: 0, width: CGFLOAT_MIN, height: CGFLOAT_MIN)
        attachment.image = UIImage()
        let attachmentAttributedString = NSMutableAttributedString(attachment: attachment)
        let style = NSMutableParagraphStyle()
        style.alignment = .center
        attachmentAttributedString.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: attachmentAttributedString.length))
        
        attributedText.append(attachmentAttributedString)
        attributedText.append(("\n"+after).formattedText())
        
        originalAttributedText = attributedText
        myTextNode.attributedText = attributedText
    }

    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        let paddingToUse = 10.0
        return ASInsetLayoutSpec(insets: UIEdgeInsets(top: paddingToUse, left: paddingToUse, bottom: paddingToUse, right: paddingToUse), child: myTextNode)
    }
    
    override func didEnterPreloadState() {
        super.didEnterPreloadState()
        print("----- didEnterPreloadState: \(String(describing: debugName))")
        
        guard let attributedText = originalAttributedText else { return }
        
        attributedText.enumerateAttribute(NSAttributedString.Key.attachment, in: NSRange(location: 0, length: attributedText.length)) { a, range, _ in
            guard let attachment = a as? CustomAttachment else { return }
            
            // 异步加载图片,避免阻塞主线程
            DispatchQueue.global().async {
                if let imageData = try? Data(contentsOf: attachment.url), let img = UIImage(data: imageData) {
                    DispatchQueue.main.async {
                        print("Size: \(img.size)")
                        // 更新attachment的图片和bounds
                        attachment.image = img
                        // 按宽高比计算目标尺寸,保证图片比例正确
                        let targetWidth = 200.0
                        let scale = targetWidth / img.size.width
                        let targetHeight = img.size.height * scale
                        attachment.bounds = CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight)
                        
                        // 重新设置attributedText,触发ASTextNode更新
                        self.myTextNode.attributedText = self.originalAttributedText
                    }
                }
            }
        }
    }
    
    override func didExitPreloadState() {
        super.didExitPreloadState()
        print("----- didExitPreloadState: \(String(describing: debugName))")
    }
}

extension String {
    func formattedText() -> NSAttributedString {
        return NSAttributedString(string: self, attributes: [
            NSAttributedString.Key.foregroundColor : UIColor.white,
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .regular)
        ])
    }
}

extension NSMutableAttributedString {
    func addAttribute(_ name: NSAttributedString.Key, value: Any) {
        addAttribute(name, value: value, range: NSRange(location: 0, length: length))
    }

    func addAttributes(_ attrs: [NSAttributedString.Key : Any]) {
        addAttributes(attrs, range: NSRange(location: 0, length: length))
    }
}

class CustomAttachment: NSTextAttachment {
    var url : URL
    
    public init(url: URL) {
        self.url = url
        super.init(data: nil, ofType: nil)
    }
      
    required init?(coder: NSCoder) {
        fatalError()
    }
}

关键修改点

  1. 新增originalAttributedText属性保存可变富文本对象,确保后续修改可追踪
  2. 将图片加载改为异步方式,避免阻塞主线程影响滑动流畅度
  3. 更新Attachment后重新设置myTextNode.attributedText,触发节点重绘
  4. 按图片原始宽高比计算目标尺寸,保证图片显示比例正确

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 05:45:59