Search code examples
iosswiftuicollectionviewuicollectionviewlayout

prepare() not called in my custom collectionView layout


I am trying to setup a custom collectionView layout and feel like I may be missing a step.

I created the collectionView, defined constraints, changed the layout from flow to custom and picked my custom class.

enter image description here

Now its not working and nothing is showing up so I am trying to debug the class method but I can't get any print statements to work because no function seems to be called. What step am I missing in setting this up? Here is the class:

import UIKit

class CustomCollectionViewLayout: UICollectionViewLayout {

    private var cache = [IndexPath: UICollectionViewLayoutAttributes]()
    private var contentWidth = CGFloat()
    private var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
    private var oldBounds = CGRect.zero
    private var collectionViewStartY: CGFloat {
        guard let collectionView = collectionView else {
            return 0
        }
        return collectionView.bounds.minY
    }
    private var collectionViewHeight: CGFloat {
        return collectionView!.frame.height
    }
    override public var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: collectionViewHeight)
    }

    override public func prepare() {
        print("calling prepare") //This is what's not printing
        guard let collectionView = collectionView,
            cache.isEmpty else {
                return
        }

        updateInsets()
        collectionView.decelerationRate = .fast
        cache.removeAll(keepingCapacity: true)
        cache = [IndexPath: UICollectionViewLayoutAttributes]()
        oldBounds = collectionView.bounds
        var xOffset: CGFloat = 0
        var cellWidth: CGFloat = 5

        for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
            let cellIndexPath = IndexPath(item: item, section: 0)
            let cellattributes = UICollectionViewLayoutAttributes(forCellWith: cellIndexPath)
            cellattributes.frame = CGRect(x: xOffset, y: 0, width: cellWidth, height: collectionViewHeight)
            xOffset = xOffset + cellWidth
            contentWidth = max(contentWidth,xOffset)
            cache[cellIndexPath] = cellattributes
        }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        visibleLayoutAttributes.removeAll(keepingCapacity: true)
        print("layoutattributes called")

        for (_, attributes) in cache {
            visibleLayoutAttributes.append(self.shiftedAttributes(from: attributes))
        }
        return visibleLayoutAttributes
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let attributes = cache[indexPath] else { fatalError("No attributes cached") }
        print("layoutattributes called")
        return shiftedAttributes(from: attributes)
    }
    override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        if oldBounds.size != newBounds.size {
            cache.removeAll(keepingCapacity: true)
        }
        return true
    }

    override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
        if context.invalidateDataSourceCounts { cache.removeAll(keepingCapacity: true) }
        super.invalidateLayout(with: context)
    }
}
extension CustomCollectionViewLayout {

    func updateInsets() {
        guard let collectionView = collectionView else { return }
        collectionView.contentInset.left = (collectionView.bounds.size.width - 2.5) / 2
        collectionView.contentInset.right = (collectionView.bounds.size.width - 2.5) / 2
    }
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
        let midX: CGFloat = collectionView.bounds.size.width / 2
        guard let closestAttribute = findClosestAttributes(toXPosition: proposedContentOffset.x + midX) else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
        return CGPoint(x: closestAttribute.center.x - midX, y: proposedContentOffset.y)
    }

    private func findClosestAttributes(toXPosition xPosition: CGFloat) -> UICollectionViewLayoutAttributes? {
        guard let collectionView = collectionView else { return nil }
        let searchRect = CGRect(
            x: xPosition - collectionView.bounds.width, y: collectionView.bounds.minY,
            width: collectionView.bounds.width * 2, height: collectionView.bounds.height
        )
        let closestAttributes = layoutAttributesForElements(in: searchRect)?.min(by: { abs($0.center.x - xPosition) < abs($1.center.x - xPosition) })
        return closestAttributes
    }
    private var continuousFocusedIndex: CGFloat {
        guard let collectionView = collectionView else { return 0 }
        let offset = collectionView.bounds.width / 2 + collectionView.contentOffset.x - 2.5 / 2
        return offset / 2.5
    }
    private func shiftedAttributes(from attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        guard let attributes = attributes.copy() as? UICollectionViewLayoutAttributes else { fatalError("Couldn't copy attributes") }
        let roundedFocusedIndex = round(continuousFocusedIndex)
        let focusedItemWidth = CGFloat(20)
        if attributes.indexPath.item == Int(roundedFocusedIndex){
            attributes.transform = CGAffineTransform(scaleX: 10, y: 1)
        } else {
            let translationDirection: CGFloat = attributes.indexPath.item < Int(roundedFocusedIndex) ? -1 : 1
            attributes.transform = CGAffineTransform(translationX: translationDirection * 20, y: 0)
        }
        return attributes
    }
}

If the class is invalid somehow would that prevent its methods from being called? What is it that auto-executes the prepare() method on layout?

And here is the view Controller where its used:

class ViewController: UIViewController, UICollectionViewDelegate,UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = customCollectionView.dequeueReusableCell(withReuseIdentifier: "singleCell", for: indexPath)
        cell.backgroundColor = UIColor.random()
        return cell
    }

    @IBOutlet weak var customCollectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        customCollectionView.delegate = self

    }
}

Solution

  • You should provide the number of sections for collection view. Otherwise, it is assumed to be 0 and, therefore, the implementation of the layout does not begin.

    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    

    Also try to debug the values of collectionView and collectionViewLayout in your view controller