我可以用这个简单的例子来演示我的问题。
我在我的 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()
}
}
我自己找到了解决方案。
基本上,首先我将
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
}
}