iosswiftuicollectionviewuicollectionviewlayoutuicollectionviewflowlayout

How to align cell to top by Flow Layout in CollectionView


Image1

In this code I am trying to change the size of the first cell of the UICollectionView and the others with the same size but in the first row only one cell is coming out when I want two to come out:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:
    if indexPath.row == 0 {
        return CGSize(width: collectionView.frame.width/1.5-2, height: collectionView.frame.width/1.5-2)
    }
    else {
        return CGSize(width: collectionView.frame.width/3.0-3, height: collectionView.frame.width/3.0-3)
    }
}

What I really want is this:

Image2


Solution

  • You need to implement an UICollectionViewLayout, I had called it FillingLayout, Note that you can adjust the number of columns and the size of your big cells with the delegate methods

    Explanation

    You need to add an Array to track your columns heigths and see what is the shortest column that is private var columsHeights : [CGFloat] = [] and you need also an array of (Int,Float) tuple to keep which spaces are available to be filled, I also added a method in the delegate to get the number of columns we want in the collection View and a method to know if a cell can be added or not in a position according their size.

    Then if we want to add a cell we check if can be added if not, because the first column is filled we add the space corresponding to column2 in the avaiableSpaces array and when we add the next cell first we check if can be added in any available space if can be added we add and remove the available space.

    here is the full code

    import UIKit
    
    protocol FillingLayoutDelegate: class {
        func collectionView(_ collectionView:UICollectionView, sizeForViewAtIndexPath indexPath:IndexPath) -> Int
        //  Returns the amount of columns that have to display at that moment
        func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int
    }
    
    class FillingLayout: UICollectionViewLayout {
        weak var delegate: FillingLayoutDelegate!
    
        fileprivate var cellPadding: CGFloat = 10
    
        fileprivate var cache = [UICollectionViewLayoutAttributes]()
    
        fileprivate var contentHeight: CGFloat = 0
        private var columsHeights : [CGFloat] = []
        private var avaiableSpaces : [(Int,CGFloat)] = []
    
        fileprivate var contentWidth: CGFloat {
            guard let collectionView = collectionView else {
                return 0
            }
            let insets = collectionView.contentInset
            return collectionView.bounds.width - (insets.left + insets.right)
        }
    
        var columnsQuantity : Int{
            get{
                if(self.delegate != nil)
                {
                    return (self.delegate?.numberOfColumnsInCollectionView(collectionView: self.collectionView!))!
                }
                return 0
            }
        }
    
        //MARK: PRIVATE METHODS
        private func shortestColumnIndex() -> Int{
            var retVal : Int = 0
            var shortestValue = MAXFLOAT
    
            var i = 0
            for columnHeight in columsHeights {
                //debugPrint("Column Height: \(columnHeight) index: \(i)")
                if(Float(columnHeight) < shortestValue)
                {
                    shortestValue = Float(columnHeight)
                    retVal = i
                }
                i += 1
            }
            //debugPrint("shortest Column index: \(retVal)")
            return retVal
        }
    
        //MARK: PRIVATE METHODS
        private func largestColumnIndex() -> Int{
            var retVal : Int = 0
            var largestValue : Float = 0.0
    
            var i = 0
            for columnHeight in columsHeights {
                //debugPrint("Column Height: \(columnHeight) index: \(i)")
                if(Float(columnHeight) > largestValue)
                {
                    largestValue = Float(columnHeight)
                    retVal = i
                }
                i += 1
            }
            //debugPrint("shortest Column index: \(retVal)")
            return retVal
        }
    
        private func canUseBigColumnOnIndex(columnIndex:Int,size:Int) ->Bool
        {
            if(columnIndex < self.columnsQuantity - (size-1))
            {
                let firstColumnHeight = columsHeights[columnIndex]
                for i in columnIndex..<columnIndex + size{
                    if(firstColumnHeight != columsHeights[i]) {
                        return false
                    }
                }
                return true
            }
    
            return false
        }
    
        override var collectionViewContentSize: CGSize {
            return CGSize(width: contentWidth, height: contentHeight)
        }
    
        override func prepare() {
            // Check if cache is empty
            guard cache.isEmpty == true, let collectionView = collectionView else {
                return
            }
    
            //  Set all column heights to 0
            self.columsHeights = []
            for _ in 0..<self.columnsQuantity {
                self.columsHeights.append(0)
            }
    
            for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
    
                let indexPath = IndexPath(item: item, section: 0)
    
                let viewSize: Int = delegate.collectionView(collectionView, sizeForViewAtIndexPath: indexPath)
                let blockWidth = (contentWidth/CGFloat(columnsQuantity))
                let width = blockWidth * CGFloat(viewSize)
                let height = width
    
                var columIndex = self.shortestColumnIndex()
                var xOffset = (contentWidth/CGFloat(columnsQuantity)) * CGFloat(columIndex)
                var yOffset = self.columsHeights[columIndex]
    
                if(viewSize > 1){//Big Cell
                    if(!self.canUseBigColumnOnIndex(columnIndex: columIndex,size: viewSize)){
                        //  Set column height
                        for i in columIndex..<columIndex + viewSize{
                            if(i < columnsQuantity){
                                self.avaiableSpaces.append((i,yOffset))
                                self.columsHeights[i] += blockWidth
                            }
                        }
                        //  Set column height
                        yOffset = columsHeights[largestColumnIndex()]
                        xOffset = 0
                        columIndex = 0
                    }
    
                    for i in columIndex..<columIndex + viewSize{
                        if(i < columnsQuantity){
                            //current height
                            let currValue = self.columsHeights[i]
                            //new column height with the update
                            let newValue = yOffset + height
                            //space that will remaing in blank, this must be 0 if its ok
                            let remainder = (newValue - currValue) - CGFloat(viewSize) * blockWidth
                            if(remainder > 0) {
                                debugPrint("Its bigger remainder is \(remainder)")
                                //number of spaces to fill
                                let spacesTofillInColumn = Int(remainder/blockWidth)
                                //we need to add those spaces as avaiableSpaces
                                for j in 0..<spacesTofillInColumn {
                                    self.avaiableSpaces.append((i,currValue + (CGFloat(j)*blockWidth)))
                                }
                            }
                            self.columsHeights[i] = yOffset + height
                        }
                    }
                }else{
                    //if there is not avaiable space
                    if(self.avaiableSpaces.count == 0)
                    {
                        //  Set column height
                        self.columsHeights[columIndex] += height
                    }else{//if there is some avaiable space
                        yOffset = self.avaiableSpaces.first!.1
                        xOffset = CGFloat(self.avaiableSpaces.first!.0) * width
                        self.avaiableSpaces.remove(at: 0)
                    }
                }
    
                print(width)
    
                let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
                let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
    
                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                attributes.frame = insetFrame
                cache.append(attributes)
    
                contentHeight = max(contentHeight, frame.maxY)
            }
        }
    
        func getNextCellSize(currentCell: Int, collectionView: UICollectionView) -> Int {
            var nextViewSize = 0
            if currentCell < (collectionView.numberOfItems(inSection: 0) - 1) {
                nextViewSize = delegate.collectionView(collectionView, sizeForViewAtIndexPath: IndexPath(item: currentCell + 1, section: 0))
            }
            return nextViewSize
        }
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    
            var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
    
            // Loop through the cache and look for items in the rect
            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]
        }
    }
    

    UPDATED

    You need to setup your viewController as FillingLayoutDelegate

    override func viewDidLoad() {
        super.viewDidLoad()
    
        collectionView.delegate = self
        collectionView.dataSource = self
    
        // Do any additional setup after loading the view.
        if let layout = self.collectionView.collectionViewLayout as? FillingLayout
        {
            layout.delegate = self
        }
    
    }
    

    FillingLayoutDelegate implementation in your ViewController

    extension ViewController: FillingLayoutDelegate{
    func collectionView(_ collectionView:UICollectionView,sizeForViewAtIndexPath indexPath:IndexPath) ->Int{
            if(indexPath.row == 0 || indexPath.row == 4)
            {
                return 2
            }
    
            if(indexPath.row == 5)
            {
                return 3
            }
    
            return 1
        }
    
        func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int{
            return 3
        }
    }
    

    ScreenShot working

    enter image description here