Search code examples
iosswiftobjective-cflexboxasyncdisplaykit

iOS AsyncDisplayKit / Texture's ASAbsoluteLayoutSpec not stretching content to fill width


I am using AsyncDisplayKit/Texture in my iOS app to display a simple list. On each row, I want to display some text and an image horizontally and at the very bottom right corner of the row, I want to display a triangle. I have the following simple code which can demonstrate the entire issue:

import UIKit
import SnapKit
import AsyncDisplayKit

class ViewController: UIViewController, ASTableDataSource {

    let tableNode = ASTableNode()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubnode(tableNode)
        tableNode.view.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        tableNode.dataSource = self
    }

    func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
        return 100
    }
    
    func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
        let row = indexPath.row
        return {
            return MyCellNode(str: "Row \(row)")
        }
    }

}

let sizeToUse = 15.0

class MyCellNode: ASCellNode {
    private var title = ASTextNode()
    private let savedIcon = SaveIconNode()
    private var thumbnailNode = ASNetworkImageNode()
    
    init(str : String) {
        super.init()
        automaticallyManagesSubnodes = true
        automaticallyRelayoutOnSafeAreaChanges = true
        automaticallyRelayoutOnLayoutMarginsChanges = true
        
        title.attributedText = NSAttributedString(string: str, attributes: [.foregroundColor:UIColor.white,.font:UIFont.systemFont(ofSize: 30, weight: .medium)])
        title.backgroundColor = .darkGray
        
        thumbnailNode = ASNetworkImageNode()
        thumbnailNode.isLayerBacked = true
        thumbnailNode.cornerRoundingType = .precomposited
        thumbnailNode.cornerRadius = sizeToUse
        thumbnailNode.style.preferredSize = CGSize(width: 60, height: 60)
        thumbnailNode.url = URL(string: "https://favicon.mars51.com/instagram.com")
    }
    
    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        
        
        
        let content = ASStackLayoutSpec(direction: .horizontal, spacing: sizeToUse, justifyContent: .spaceBetween, alignItems: .start, children: [title,thumbnailNode])
        
        
        let inseted = ASInsetLayoutSpec(insets: UIEdgeInsets(top: sizeToUse, left: sizeToUse, bottom: sizeToUse, right: sizeToUse), child: content)
        
        
        savedIcon.style.preferredSize = CGSize(width: sizeToUse, height: sizeToUse)
        savedIcon.style.layoutPosition = CGPoint(x: constrainedSize.max.width - sizeToUse, y: 0)
        
        let finalLayout = ASAbsoluteLayoutSpec(sizing: .default, children: [inseted,savedIcon])
        
        
        return finalLayout
    }
    
    override func layout() {
        super.layout()
        var f = savedIcon.frame
        f.origin.y = calculatedSize.height - f.height
        savedIcon.frame = f
    }
}

class SaveIconNode: ASDisplayNode {
    
    override init() {
        super.init()
        isLayerBacked = true
        backgroundColor = .clear
        isOpaque = false
    }
    
    override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled isCancelledBlock: () -> Bool, isRasterizing: Bool) {
        
        guard let context = UIGraphicsGetCurrentContext() else { return }

        context.beginPath()
        context.move(to: CGPoint(x: bounds.maxX, y: bounds.minY))
        context.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
        context.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
        context.closePath()

        UIColor.green.setFill()
        context.fillPath()
    }
}

This renders as such:

enter image description here

As you can see, the content in the rows aren't stretching the entire width of the row. This seems to be caused by ASAbsoluteLayoutSpec. I am not sure why it's happening and how to fix it. Note that I have specified sizing: .default on the ASAbsoluteLayoutSpec and documents state that:

https://texturegroup.org/docs/layout2-layoutspec-types.html

sizing: Determines how much space the absolute spec will take up. Options include: Default, and Size to Fit.

And in the source code, it says default will The spec will take up the maximum size possible.:

/** How much space the spec will take up. */
typedef NS_ENUM(NSInteger, ASAbsoluteLayoutSpecSizing) {
  /** The spec will take up the maximum size possible. */
  ASAbsoluteLayoutSpecSizingDefault,
  /** Computes a size for the spec that is the union of all children's frames. */
  ASAbsoluteLayoutSpecSizingSizeToFit,
};

So I think my code is correct.

If I don't include the ASAbsoluteLayoutSpec and simply return inseted, then it looks okay:

enter image description here

But then I am not able to include my savedIcon triangle.

I have tried setting .style.flexGrow = 1 and .style.alignSelf = .stretch on all the nodes but that didn't make any difference whatsoever.


Solution

  • I figured out the solution in a different way.

    1. I got rid of the func layout() function.

    2. I put the savedIcon inside an ASInsetLayoutSpec whose top and left are set to CGFloat.infinity.

    3. I returned a ASOverlayLayoutSpec whose child is the inseted content and overlay is the above created savedIconSpec.

    I learnt about this trick from the Photo with Inset Text Overlay example below:

    https://texturegroup.org/docs/automatic-layout-examples-2.html

    Now my layoutSpecThatFits looks like this:

    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
            
        let content = ASStackLayoutSpec(direction: .horizontal, spacing: sizeToUse, justifyContent: .spaceBetween, alignItems: .start, children: [title,thumbnailNode])
        
        let inseted = ASInsetLayoutSpec(insets: UIEdgeInsets(top: sizeToUse, left: sizeToUse, bottom: sizeToUse, right: sizeToUse), child: content)
        
        savedIcon.style.preferredSize = CGSize(width: sizeToUse, height: sizeToUse)
        savedIcon.style.layoutPosition = CGPoint(x: constrainedSize.max.width - sizeToUse, y: 0)
        
        let savedIconSpec = ASInsetLayoutSpec(insets: UIEdgeInsets(top: CGFloat.infinity, left: CGFloat.infinity, bottom: 0, right: 0), child: savedIcon)
        
        return ASOverlayLayoutSpec(child: inseted, overlay: savedIconSpec)
    }
    

    It looks like this now:

    enter image description here