Asyncdisplaykit: Dynamic collection view layout

Created on 28 Jul 2016  路  2Comments  路  Source: facebookarchive/AsyncDisplayKit

My team are implementing a fancy collection layout, where items change their size and offset as they are scrolled. We had already implemented it _sans_ ASDK by subclassing UICollectionViewLayout, roughly similar to this tutorial. Our subclass is called UltraVisualLayout. Its implementation is long and math-y, but the important bits basically go like this:

class UltravisualLayout: UICollectionViewLayout {
    var cache = [UICollectionViewLayoutAttributes]()

    override func prepareLayout() {
        cache.removeAll(keepCapacity: false)
        for item in 0..<numberOfItems {
            // calculate x, y and h...
            let frame = CGRect(x: x, y: y, width: width - (2 * x), height: h)
            attributes.frame = frame
            attributes.zIndex = item
            cache.append(attributes)
        }
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var layoutAttributes = [UICollectionViewLayoutAttributes]()
        for attributes in cache {
            if CGRectIntersectsRect(attributes.frame, rect) {
                layoutAttributes.append(attributes)
            }
        }
        return layoutAttributes
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        var layoutAttributes: UICollectionViewLayoutAttributes?
        for attributes in cache {
            if attributes.indexPath == indexPath {
                layoutAttributes = attributes
            }
        }
        return layoutAttributes
    }

    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }
}

So basically, as the user scrolls, the layouts are continually invalidated, recalculated, and stored in a cache, from whence they are fetched upon the collection view's request.

ASDK is very cool, and so we decided to convert our collection view controller to an ASViewController with an ASCollectionNode. We init them as follows:

init {
    super.init(node: ASCollectionNode(collectionViewLayout: UltraVisualLayout()))
    // etc...
}

We subclassed ASCollectionViewLayoutInspecting to interact with the layout as follows:

class UltravisualLayoutInspector: NSObject, ASCollectionViewLayoutInspecting {
    func collectionView(collectionView: ASCollectionView!, constrainedSizeForNodeAtIndexPath indexPath: NSIndexPath!) -> ASSizeRange {
        return ASSizeRangeMake(CGSizeZero, layout.itemSizeAtIndexPath(indexPath))
    }

    func collectionView(collectionView: ASCollectionView!, constrainedSizeForSupplementaryNodeOfKind kind: String!, atIndexPath indexPath: NSIndexPath!) -> ASSizeRange {
        return ASSizeRange(min: CGSizeZero, max: CGSizeZero)
    }

    func collectionView(collectionView: ASCollectionView!, numberOfSectionsForSupplementaryNodeOfKind kind: String!) -> UInt {
        return 0
    }

    func collectionView(collectionView: ASCollectionView!, supplementaryNodesOfKind kind: String!, inSection section: UInt) -> UInt {
        return 0
    }
}

The layout still works on the UI side of things, apparently. Using XCode's visual inspector thingy, I can see that the frames are updating correctly (size and offset-wise). But when viewing the app itself, things are wrong. The display nodes do not update their sizes as they scroll. They do update their offsets though.

We think it might be because there is no ASDK equivalent to UICollectionViewLayout's shouldInvalidateLayoutForBoundsChange, so we can't notify the collection node that it should recalculate its display nodes' layout specs. We tried things like this

func scrollViewDidScroll(scrollView: UIScrollView) {
    collectionNode.invalidateCalculatedLayout()
}

but to no effect.

How could we approach this?

Most helpful comment

We found that updating the frame of a view in a collection did not have the desired effect, but applying a transform to an item's attributes in the UICollectionViewLayout subclass works! We are now using something like

class UltravisualLayout: UICollectionViewLayout {
    override func prepareLayout() {
        cache.removeAll(keepCapacity: false)
        for item in 0..<numberOfItems {
            // calculate x, y and h...
            let frame = CGRect(x: x, y: y, width: width - (2 * x), height: h)
            attributes.frame = frame
            attributes.zIndex = item

            let scale = (width - (2 * x)) / width
            attributes.transform = CGAffineTransformMakeScale(scale, scale) // this works

            cache.append(attributes)
        }
    }
    // ...
}

All 2 comments

We found that updating the frame of a view in a collection did not have the desired effect, but applying a transform to an item's attributes in the UICollectionViewLayout subclass works! We are now using something like

class UltravisualLayout: UICollectionViewLayout {
    override func prepareLayout() {
        cache.removeAll(keepCapacity: false)
        for item in 0..<numberOfItems {
            // calculate x, y and h...
            let frame = CGRect(x: x, y: y, width: width - (2 * x), height: h)
            attributes.frame = frame
            attributes.zIndex = item

            let scale = (width - (2 * x)) / width
            attributes.transform = CGAffineTransformMakeScale(scale, scale) // this works

            cache.append(attributes)
        }
    }
    // ...
}

Going to close this out as it looks like it was figured out.

Was this page helpful?
0 / 5 - 0 ratings