UICollectionView autosize and dynamic number of rows

谁都会走 提交于 2019-12-04 07:36:17

I can suggest you to create your own UICollectionViewFlowLayout subclass which would generate needed layout. Here is a simple flow layout that you can use. You can adjust it to your needs, if I missed something.

public protocol CollectionViewFlowLayoutDelegate: class {
    func numberOfColumns() -> Int
    func height(at indexPath: IndexPath) -> CGFloat
}

public class CollectionViewFlowLayout: UICollectionViewFlowLayout {
    private var cache: [IndexPath : UICollectionViewLayoutAttributes] = [:]
    private var contentHeight: CGFloat = 0
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else {
            return 0
        }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }

    public weak var flowDelegate: CollectionViewFlowLayoutDelegate?

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

    public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var layoutAttributesArray = [UICollectionViewLayoutAttributes]()
        if cache.isEmpty {
            self.prepare()
        }
        for (_, layoutAttributes) in self.cache {
            if rect.intersects(layoutAttributes.frame) {
                layoutAttributesArray.append(layoutAttributes)
            }
        }
        return layoutAttributesArray
    }

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

    public override func prepare() {
        guard let collectionView = self.collectionView else {
            return
        }
        let numberOfColumns = self.flowDelegate?.numberOfColumns() ?? 1
        let cellPadding: CGFloat = 8
        self.contentHeight = 0
        let columnWidth = UIScreen.main.bounds.width / CGFloat(numberOfColumns)
        var xOffset = [CGFloat]()
        for column in 0 ..< numberOfColumns {
            xOffset.append(CGFloat(column) * columnWidth)
        }
        var column = 0
        var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)

        for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
            let indexPath = IndexPath(item: item, section: 0)
            let photoHeight = self.flowDelegate?.height(at: indexPath) ?? 1
            let height = cellPadding * 2 + photoHeight
            let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
            let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            self.cache[indexPath] = attributes
            self.contentHeight = max(self.contentHeight, frame.maxY)
            yOffset[column] = yOffset[column] + height
            column = column < (numberOfColumns - 1) ? (column + 1) : 0
        }
    }
}

Now in your UIViewController you can use it like this:

override func viewDidLoad() {
    super.viewDidLoad()
    let flowLayout = CollectionViewFlowLayout()
    flowLayout.flowDelegate = self
    self.collectionView.collectionViewLayout = flowLayout
}

invalidate collectionView layout after orientation would be changed

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    guard let flowLayout = self.collectionView.collectionViewLayout as? CollectionViewFlowLayout else {
        return
    }
    flowLayout.invalidateLayout()
}

and now make your ViewController conform to CollectionViewFlowLayoutDelegate

extension ViewController: CollectionViewFlowLayoutDelegate {
    func height(at indexPath: IndexPath) -> CGFloat {
        guard let cell = self.collectionView.cellForItem(at: indexPath) else {
            return 1
        }
        cell.layoutIfNeeded()
        //get calculated cell height
        return cell.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
    }

    func numberOfColumns() -> Int {
        //how much columns would be shown, depends on orientation
        return UIDevice.current.orientation == .portrait ? 2 : 3
    }
}

This could be another alternative:

protocol MyLayoutDelegate: class {
    func collectionView(_ collectionView: UICollectionView, heightForCellAtIndexPath indexPath: IndexPath, width: CGFloat) -> CGFloat
    func numberOfColumns() -> Int
}

import UIKit

class MyCollectionViewLayout: UICollectionViewLayout {

    weak var delegate: MyLayoutDelegate!


    public var cellPadding: CGFloat = 5.0

    var cellWidth: CGFloat = 190.0
    var cachedWidth: CGFloat = 0.0
    var minimumInteritemSpacing: CGFloat = 10

    var numberOfColumns = 1

    fileprivate var cache = [UICollectionViewLayoutAttributes]()
    fileprivate var contentHeight: CGFloat  = 0.0
    fileprivate var contentWidth: CGFloat {
        if let collectionView = collectionView {
            let insets = collectionView.contentInset
            return collectionView.bounds.width - (insets.left + insets.right)
        }
        return 0
    }

    fileprivate var numberOfItems = 0

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

    override func prepare() {

        guard let collectionView = collectionView else { return }

        self.numberOfColumns = delegate.numberOfColumns()

        cellWidth = (contentWidth - minimumInteritemSpacing * 2) / CGFloat(numberOfColumns)

        let totalSpaceWidth = contentWidth - CGFloat(numberOfColumns) * cellWidth
        let horizontalPadding = totalSpaceWidth / CGFloat(numberOfColumns + 1)
        let numberOfItems = collectionView.numberOfItems(inSection: 0)
        if (contentWidth != cachedWidth || self.numberOfItems != numberOfItems) {
            cache = []
            contentHeight = 0
            self.numberOfItems = numberOfItems
        }

        cachedWidth = contentWidth
        var xOffset = [CGFloat]()
        for column in 0 ..< numberOfColumns {
            xOffset.append(CGFloat(column) * cellWidth + CGFloat(column + 1) * horizontalPadding)
        }
        var column = 0
        var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
        for row in 0 ..< numberOfItems {
            let indexPath = IndexPath(row: row, section: 0)
            let cellHeight = delegate.collectionView(collectionView, heightForCellAtIndexPath: indexPath, width: cellWidth)
            let height = cellPadding * 2 +  cellHeight
            let frame = CGRect(x: xOffset[column], y: yOffset[column], width: cellWidth, height: height)
            let insetFrame = frame.insetBy(dx: 0, dy: cellPadding)
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
            attributes.frame = insetFrame
            cache.append(attributes)
            contentHeight = max(contentHeight, frame.maxY)
            yOffset[column] = yOffset[column] + height
            if column >= (numberOfColumns - 1) {
                column = 0
            } else {
                column = column + 1
            }
        }
}

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

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

}

To use this:

1) In the storyboard select your collectionView ( Es. myCollectionView ) and search for CollectionViewLayout 2) Click on the class inspector and change the class to MyCollectionViewLayout.

3) In your viewDidLoad:

if let layout = myCollectionView?.collectionViewLayout as MyCollectionViewLayout {
        layout.delegate = self
    }

4) Add the two protocol stubs:

func collectionView(_ collectionView: UICollectionView, heightForCellAtIndexPath indexPath: IndexPath, width: CGFloat) -> CGFloat {
    //default height for different layout
    guard let layout = collectionView.collectionViewLayout as? MyCollectionViewLayout else{return 150 // or anything else}

    //return what you want, even based on screen size
    return 100

}

func numberOfColumns() -> Int{
   //return number of columns based on screen size

   return 1 // or whatever you want
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!