Open Pranoy1c opened 11 months ago
I figured out the solution myself.
Basically, first I set the attributedText
value of the ASTextNode
to be an NSAttributedString
with a custom subclass of NSTextAttachment
with a URL
variable and a custom implementation for attachmentBounds
function (that will take care of providing the correct bounds for the image depending upon its aspect ratio). Then, in didEnterPreloadState
, I enumerate the ranges for this attachment
and async download the images using SDWebImageManager
(not necessary though). Once done, I replaceCharacters
of the original NSAttributedString
to replace the original NSTextAttachment
with a one whose image
property is set to this fetched image. Then I set the attributedText
of the ASTextNode
to this updated attributedText
again and call invalidateCalculatedLayout
and setNeedsLayout
to update the display.
Here's the full demo code:
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
}
}
I am able to demonstrate my issue with this simple example.
I have a
ASTableNode
which shows attributed strings. These will have images in them viaNSTextAttachment
. These images will be from URLs which will be downloaded asynchronously. In this example, for simplicity, I am just using an image from theBundle
. Once downloaded, theNSTextAttachment
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()
andlayoutIfNeeded()
after updating the image and bounds of the NSTextAttachment after getting the image, theASTextNode
never updates to show the image. I am not sure what I am missing.Code: