Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView autosize and dynamic number of rows

I am trying to do something like this:

enter image description here Basically, I am using a UICollectionView and the cells (3 diferent .xib).

So far, it works.

The thing I want to do is:

  1. Set a autoheight
  2. If rotate, add 1 row to the UIColectionView 2.1 If tablet, on portrait will have 2 rows and landscape 3 rows. (which basically is the same of point 2, only adding 1 row.

I have something like this:

extension ViewController {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator){
    setSizeSize()
}

func setSizeSize(){
    if(DeviceType.IS_IPAD || UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight){
        if let layout = myCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            layout.estimatedItemSize = CGSize(width: 1, height: 1)
            layout.invalidateLayout()
        }
    }else{
        if let layout = myCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
            layout.invalidateLayout()
        }
    }
    myCollectionView.collectionViewLayout.invalidateLayout()
}
}

Does not work. Also, it freezes device. On simulator works parcially. (I'm trusting more device)

I also tried this, but it works sometimes...

Please, let me know if you need more info.

Thank you all in advance for the help.

like image 779
Amg91 Avatar asked Nov 01 '18 22:11

Amg91


2 Answers

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
    }
}
like image 186
Taras Chernyshenko Avatar answered Sep 25 '22 22:09

Taras Chernyshenko


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
}
like image 28
Andrew21111 Avatar answered Sep 23 '22 22:09

Andrew21111