UICollectionViewCell cellSize doesn't work for last item

Viewed 77

I am trying to replicate the AppStore Today's card view layout

Here is my code

extension TestViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 4
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let genericCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)

    if let label = genericCell.viewWithTag(100) as? UILabel {
        label.text = "\(indexPath.item)"
    }
    return genericCell
}
}

extension TestViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 0
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 40
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let cellHeight: CGFloat = 100

    let numberOfItemsInRow = 2
    let compressedWidth = collectionView.bounds.width / 2
    let expandedWidth = compressedWidth * 0.5
    let rowNumber = indexPath.item / numberOfItemsInRow
    let isEvenRow = rowNumber % 2 == 0
    let isFirstItem = indexPath.item % numberOfItemsInRow == 0

    var width: CGFloat = 0.0
    if isEvenRow {
        width = isFirstItem ? expandedWidth : compressedWidth
    } else {
        width = isFirstItem ? compressedWidth : expandedWidth
    }
    return CGSize(width: width, height: cellHeight)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20)
}
}

Above code will work fine for odd number of dataSource. If it is even number, the last row of item will not have any white spacing in between.

enter image description here

The way that I use to overcome this problem is by adding additional 1 cell, with the automated width size but with 0 height.

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

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let cellHeight: CGFloat = indexPath.item == collectionView.numberOfItems(inSection: 0) - 1 ? 0 : 100 
}

enter image description here However I don't think that this is the best approach to use. Can someone guide me on what to do? What am I missing here? Thank you

1 Answers

The best way for you is to use custom UICollectionViewFlowLayout

class TestCollectionViewLayout: UICollectionViewFlowLayout {

    var needUpdate: Bool = false

    fileprivate var cache = [UICollectionViewLayoutAttributes]()

    fileprivate var contentHeight: CGFloat = 0

    fileprivate var contentWidth: CGFloat { return collectionView?.frame.width ?? 0.0 }

    override var collectionViewContentSize: CGSize { return CGSize(width: contentWidth, height: contentHeight) }

    override func invalidateLayout() {
        needUpdate = true
        super.invalidateLayout()
    }

    override func prepare() {

        guard let collectionView = collectionView else { return }

        guard cache.isEmpty || needUpdate else {
            return
        }

        needUpdate = false

        cache = []

        contentHeight = 0

        let interitemSpacing: CGFloat = resolveMinimumInteritemSpacingForSectionAt(0)

        var yOffset: CGFloat = 0.0

        var previousItemSize: CGSize!

        for item in 0 ..< collectionView.numberOfItems(inSection: 0) {

            let indexPath = IndexPath(item: item, section: 0)

            let itemSize = resolveSizeForItem(at: indexPath)

            var point: CGPoint

            let isEvenColumn = item % 2 == 0

            if isEvenColumn {
                point = CGPoint(x: 0.0, y: yOffset + interitemSpacing)
            } else {
                point = CGPoint(x: previousItemSize.width + interitemSpacing, y: yOffset + interitemSpacing)
            }

            let frame = CGRect(origin: point, size: itemSize)

            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = frame
            cache.append(attributes)

            contentHeight = max(contentHeight, frame.maxY)

            previousItemSize = frame.size

            if !isEvenColumn {
                yOffset = frame.maxY
            }
        }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
        for attributes in cache {
            if attributes.frame.intersects(rect) {
                visibleLayoutAttributes.append(attributes)
            }
        }
        return visibleLayoutAttributes
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache[indexPath.item]
    }

}

extension TestCollectionViewLayout {

    func resolveCollectionView<T>(_ closure: (UICollectionView) -> T?, defaultValue: T) -> T {
        if let collectionView = collectionView {
            return closure(collectionView) ?? defaultValue
        } else {
            return defaultValue
        }
    }

    func resolveSizeForItem(at indexPath: IndexPath) -> CGSize {
        let size = resolveCollectionView({ collectionView -> CGSize? in
            return (collectionView.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView,
                                                                                                   layout: self,
                                                                                                   sizeForItemAt: indexPath)
        }, defaultValue: CGSize(width: 150.0, height: 100.0))

        return size
    }

    func resolveMinimumInteritemSpacingForSectionAt(_ section: Int) -> CGFloat {
        let space = resolveCollectionView({ collectionView -> CGFloat? in
            return (collectionView.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView,
                                                                                                   layout: self,
                                                                                                   minimumInteritemSpacingForSectionAt: section)
        }, defaultValue: 10.0)

        return space
    }
}

Implement 2 functions from UICollectionViewDelegateFlowLayout

extension TestViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 10
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let cellHeight: CGFloat = 100

    switch indexPath.item {
    case 0:
        return CGSize(width: (collectionView.bounds.width - 10) * 0.25, height: cellHeight)
    case 1:
    return CGSize(width: (collectionView.bounds.width - 10) * 0.75, height: cellHeight)
    case 2:
    return CGSize(width: (collectionView.bounds.width - 10) * 0.4, height: cellHeight)
    case 3:
    return CGSize(width: (collectionView.bounds.width - 10) * 0.6, height: cellHeight)
    default:
        return CGSize.zero
    }
    }
}

use this layout like regular one

let layout = TestCollectionViewLayout()
layout.scrollDirection = .vertical
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
Related