当在 ASTextNode 的属性文本中使用 AsyncDisplayKit/Texture 和 NSTextAttachment 图像时,如何重新加载 ASTextNode?

问题描述 投票:0回答:1

我可以用这个简单的例子来演示我的问题。

我在我的 iOS 应用程序中使用 AsyncDisplayKit/Texture。

我有一个

ASTableNode
,它显示属性字符串。这些将通过
NSTextAttachment
在其中包含图像。这些图像将来自异步下载的 URL。在此示例中,为简单起见,我仅使用
Bundle
中的图像。下载后,
NSTextAttachment
需要将其边界更新为实际图像的正确宽高比。

我面临的问题是,尽管我在获取图像后更新图像和 NSTextAttachment 的边界后调用

setNeedsLayout()
layoutIfNeeded()
,但
ASTextNode
永远不会更新以显示图像。我不确定我错过了什么。

代码:

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()
    
    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)
        
        attributedText.append(attachmentAttributedString)
        
        attributedText.append(("\n"+after).formattedText())
        
        myTextNode.attributedText = attributedText
        
    }

    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        
        
        if let attributedText = myTextNode.attributedText {
                        
            attributedText.enumerateAttribute(NSAttributedString.Key.attachment, in: NSRange(location: 0, length: attributedText.length)) { a, range, _ in
                if let attachment = a as? CustomAttachment {
                    print("attachment: \(attachment.bounds)")
                }
            }
        }
        
        
        let paddingToUse = 10.0
        return ASInsetLayoutSpec(insets: UIEdgeInsets(top: paddingToUse, left: paddingToUse, bottom: paddingToUse, right: paddingToUse), child: myTextNode)
    }

    override func layout() {
        super.layout()
    }
    
    override func didEnterPreloadState() {
        super.didEnterPreloadState()
        print("----- didEnterPreloadState: \(String(describing: debugName))")
        
        if let attributedText = myTextNode.attributedText {
                        
            attributedText.enumerateAttribute(NSAttributedString.Key.attachment, in: NSRange(location: 0, length: attributedText.length)) { a, range, _ in
                if let attachment = a as? CustomAttachment {
//                    print("attachment: \(attachment.url)")
                    if let imageData = NSData(contentsOf: attachment.url), let img = UIImage(data: imageData as Data) {
                        print("Size: \(img.size)")
                        attachment.image = img
                        attachment.bounds = CGRect(x: 0, y: 0, width: 200, height: 200)
                        setNeedsLayout()
                        layoutIfNeeded()
                    }
                }
            }
        }
    }
    
    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()
    }
}
ios uitableview nsattributedstring nstextattachment asyncdisplaykit
1个回答
0
投票

我自己找到了解决方案。

基本上,首先我将

attributedText
ASTextNode
值设置为
NSAttributedString
,其中包含
NSTextAttachment
的自定义子类,带有
URL
变量和
attachmentBounds
函数的自定义实现(这将照顾根据图像的长宽比提供正确的边界)。然后,在
didEnterPreloadState
中,我枚举此
attachment
的范围,并使用
SDWebImageManager
异步下载图像(尽管不是必需的)。完成后,我将原始
replaceCharacters
NSAttributedString
替换为
NSTextAttachment
属性设置为此获取的图像的原始
image
。然后我再次将
attributedText
ASTextNode
设置为更新后的
attributedText
,并调用
invalidateCalculatedLayout
setNeedsLayout
来更新显示。

这是完整的演示代码:

import UIKit
import AsyncDisplayKit
import SDWebImage

struct Item {
    var index : Int
    var before : String
    var after : String
    var image : String
}

class ViewController: ASDKViewController<ASDisplayNode>, ASTableDataSource {

    let tableNode = ASTableNode()
    let imagesToEmbed = ["https://images.unsplash.com/photo-1682686581264-c47e25e61d95?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
                         "https://plus.unsplash.com/premium_photo-1700391547517-9d63b8a8b351?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
                         "https://images.unsplash.com/photo-1682686580849-3e7f67df4015?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
                         "https://i.ytimg.com/vi/dBymYOAvgdA/maxresdefault.jpg",
                         "https://i.ytimg.com/vi/q2DBeby7ni8/maxresdefault.jpg",
                         "https://i.ytimg.com/vi/-28apOHT9Rk/maxresdefault.jpg",
                         "https://i.ytimg.com/vi/O4t8hAEEKI4/maxresdefault.jpg"
    ]
    
    override init() {
        super.init(node: tableNode)
        tableNode.dataSource = self
        tableNode.setTuningParameters(ASRangeTuningParameters(leadingBufferScreenfuls: 3, trailingBufferScreenfuls: 3), for: .preload)
        tableNode.setTuningParameters(ASRangeTuningParameters(leadingBufferScreenfuls: 3, trailingBufferScreenfuls: 3), for: .display)
    }

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

    func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
        return imagesToEmbed.count
    }
    
    func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
        let row = indexPath.row
        let img = imagesToEmbed[row]
        return {
            let node = MyCellNode(item: Item(index: row, before: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum is simply dummy text of the printing and typesetting industry.", after: "Contrary to popular belief, Lorem Ipsum is not simply random text. Lorem Ipsum is simply dummy text of the printing and typesetting industry.",image: img))
            return node
        }
    }
}

class MyCellNode: ASCellNode {
    
    fileprivate var myTextNode = ASTextNode()
    var item : Item
    
    init(item : Item) {
        self.item = item
        super.init()
        debugName = "Row \(item.index)"
        automaticallyManagesSubnodes = true
        automaticallyRelayoutOnSafeAreaChanges = true
        automaticallyRelayoutOnLayoutMarginsChanges = true
        
        let attributedText = NSMutableAttributedString(attributedString: ("\(item.index). "+item.before+"==\n\n").formattedText())
        attributedText.append(NSMutableAttributedString(attachment: CustomAttachment(url: URL(string: item.image)!)))
        attributedText.append(("\n\n=="+item.after).formattedText())
        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 layout() {
        super.layout()
    }
    
    override func didEnterPreloadState() {
        super.didEnterPreloadState()
        print("----- didEnterPreloadState: \(String(describing: debugName))")
        
        if let attributedText = myTextNode.attributedText {
                        
            attributedText.enumerateAttribute(NSAttributedString.Key.attachment, in: NSRange(location: 0, length: attributedText.length)) { a, range, _ in
                if let attachment = a as? CustomAttachment {
                    print("attachment: \(attachment.url)")
                    
                    SDWebImageManager.shared.loadImage(with: attachment.url) { a, b, c in
                        print("Progress: \(a), \(b), \(c)")
                    } completed: { img, data, err, cacheType, finished, url in
                        if let img = img {
                            attachment.image = img
                            
                            let attachmentAttributedString = NSMutableAttributedString(attachment: attachment)
                            let style = NSMutableParagraphStyle()
                            style.alignment = .center
                            attachmentAttributedString.addAttribute(.paragraphStyle, value: style)

                            let toEdit = NSMutableAttributedString(attributedString: attributedText)
                            toEdit.replaceCharacters(in: range, with: attachmentAttributedString)
                            
                            self.myTextNode.attributedText = toEdit
                            self.invalidateCalculatedLayout()
                            self.setNeedsLayout()
                        }
                    }
                }
            }
        }
    }
    
    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: 16, 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()
    }
    
    override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
        
        guard let image = image else {
            return .zero
        }
        
        var boundsToReturn = bounds
        boundsToReturn.size.width = min(image.size.width, lineFrag.size.width)
        boundsToReturn.size.height = image.size.height/image.size.width * boundsToReturn.size.width
//        print("attachment: \(lineFrag.size.width), \(bounds), \(image.size), \(boundsToReturn)")
        
        return boundsToReturn
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.