UICollectionView autosize and dynamic number of rows
Asked Answered
B

2

6

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.

Barriebarrientos answered 1/11, 2018 at 22:2 Comment(0)
G
10

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
    }
}
Geode answered 7/11, 2018 at 12:28 Comment(16)
Great answer, although you might want to explain what each line of your code doesOdontoid
Good Answer! I'll give it a try.Spokeshave
Works great! Exactly what I wanted! Thank you. The only thing is... what if I want the smallers cells to be centered of the bigger ones? Like on the image...Barriebarrientos
Centered horizontally and vertically, is that possible?Barriebarrientos
@Barriebarrientos yes, it's possible. You need to adjust code inside prepare() method. In for in loop we setting frames for cells. You need to calculate correct y for each cellGeode
I've been trying to do it... havent found a soulution for that. Algo, no make the cell widther only. Which means: centered vertically (smaller cells), width of all cells: screenSize/n. I will give you the bounty.Barriebarrientos
@Taras Chernyshenko, I have tried but nothing works. I want to set cells' with to collectionView.width/numColumns but does not work. Also, if possible, centered on yAxis... what am I doing wrong?Barriebarrientos
@Barriebarrientos show me what you have did so far, and I will try to help youGeode
I tried setting width=collectionView.bounds.width/numColumns-(numColumns)*cellPadding. I tried also with sizeAtIndex but changes everything. I am lost about how and where...Barriebarrientos
It worked now, but 1 of the 3 cells is not responding for the size change... I do not know what could it be...Barriebarrientos
@Barriebarrientos post code that you have right now and I will try to help youGeode
I do not have that much code... I am using what you did. The thing is that only on one cell is not doing what it is supposed to. Literally, I have your code, but it is not affecting the size on 1 cell type. Do you want me to post the .xib? (How do I?) Or which code do you want me to paste? UIViewController? Again, thank you for your help.Barriebarrientos
Got no clue what it was on that xib. I redid everything on that cell and I think I removed a safe area and worked. I think that was. Sorry for that much trouble and thank you again.Barriebarrientos
The think that I am having now is, when I reload the data (collectionView.reloadData(), the cells' height is 1, because of the height method... How do I make it work when I reload data?Barriebarrientos
That heightAt function literally saved the rest of my hair turning gray. THANK YOU!!Fatherhood
Works great for the visible cells, however when I scroll all the cells has a height of 1, how can I fix this?Krugersdorp
S
1

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
}
Spokeshave answered 7/11, 2018 at 12:58 Comment(1)
Great, thanks. Sorry for asking, but what's the difference between yours and @Taras Chernyshenko's solution?Barriebarrientos

© 2022 - 2025 — McMap. All rights reserved.