Search code examples
swiftuicollectionviewrowsscreen-rotationautosize

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.


Solution

  • 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
        }
    }