I am able to demonstrate my issue with this simple example.
I am using AsyncDisplayKit / Texture in my iOS app.
I have a ASTableNode
which shows attributed strings. These will have images in them via NSTextAttachment
. These images will be from URLs which will be downloaded asynchronously. In this example, for simplicity, I am just using an image from the Bundle
. Once downloaded, the NSTextAttachment
needs to update its bounds to the correct aspect ratio of the actual image.
The problem I am facing is that despite me calling setNeedsLayout()
and layoutIfNeeded()
after updating the image and bounds of the NSTextAttachment after getting the image, the ASTextNode
never updates to show the image. I am not sure what I am missing.
Code:
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()
}
}
I figured out the solution myself.
Basically, first I set the
attributedText
value of theASTextNode
to be anNSAttributedString
with a custom subclass ofNSTextAttachment
with aURL
variable and a custom implementation forattachmentBounds
function (that will take care of providing the correct bounds for the image depending upon its aspect ratio). Then, indidEnterPreloadState
, I enumerate the ranges for thisattachment
and async download the images usingSDWebImageManager
(not necessary though). Once done, IreplaceCharacters
of the originalNSAttributedString
to replace the originalNSTextAttachment
with a one whoseimage
property is set to this fetched image. Then I set theattributedText
of theASTextNode
to this updatedattributedText
again and callinvalidateCalculatedLayout
andsetNeedsLayout
to update the display.Here's the full demo code: