Search code examples
iosswiftuicollectionviewuicollectionviewlayout

How to remove cell space in UICollectionView Flow layout size?


I am customizing collection view layout into 3 columns [left side 2 cell & right side 1 portrait cell]. I am unable to remove top space of let side 2 cells.

Code:

import UIKit

extension UICollectionView{

    func getSize(noOfCellsInRow: Int, isPotrait: Bool = true)->CGSize{


        let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout

        let totalSpace = flowLayout.sectionInset.left
                 + flowLayout.sectionInset.right
                 + (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1))

        let size = Int((self.bounds.width - totalSpace) / CGFloat(noOfCellsInRow))

        return CGSize(width: size, height: isPotrait ? size+size : size)
    }
}

class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 3
  }

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
    cell.contentView.backgroundColor = .red
    return cell
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

      if indexPath.row == 1{
         return collectionView.getSize(noOfCellsInRow: 2,isPotrait: true)
      }else{
        return collectionView.getSize(noOfCellsInRow: 2,isPotrait: false)
     }

    }
}

Output of the corresponding code:

enter image description here

Expected output:

enter image description here

How can I make it work? Any idea will help me lot. Thanks in advance...


Solution

  • I've customized Apple's MosaicLayout example to fit your expected output.

    Here is the custom UICollectionViewLayout, with twoFiftyFifty added as this segment style.

    enum MosaicSegmentStyle {
        case twoFiftyFifty
        case fullWidth
        case fiftyFifty
        case twoThirdsOneThird
        case oneThirdTwoThirds
    }
    
    class MosaicLayout: UICollectionViewLayout {
    
        var contentBounds = CGRect.zero
        var cachedAttributes = [UICollectionViewLayoutAttributes]()
    
        /// - Tag: PrepareMosaicLayout
        override func prepare() {
            super.prepare()
    
            guard let collectionView = collectionView else { return }
    
            // Reset cached information.
            cachedAttributes.removeAll()
            contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)
    
            // For every item in the collection view:
            //  - Prepare the attributes.
            //  - Store attributes in the cachedAttributes array.
            //  - Combine contentBounds with attributes.frame.
            let count = collectionView.numberOfItems(inSection: 0)
    
            var currentIndex = 0
            var segment: MosaicSegmentStyle = .fiftyFifty
            var lastFrame: CGRect = .zero
    
            let cvWidth = collectionView.bounds.size.width
    
            while currentIndex < count {
                let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: (self.collectionView?.frame.width)!)
    
                var segmentRects = [CGRect]()
                switch segment {
                case .twoFiftyFifty:
                    let horizontalSlices = segmentFrame.dividedIntegral(fraction:0.5, from: .minXEdge)
                    let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
                    segmentRects = [verticalSlices.first, verticalSlices.second, horizontalSlices.second]
    
                case .fullWidth:
                    segmentRects = [segmentFrame]
    
                case .fiftyFifty:
                    let horizontalSlices = segmentFrame.dividedIntegral(fraction: 0.5, from: .minXEdge)
                    segmentRects = [horizontalSlices.first, horizontalSlices.second]
    
                case .twoThirdsOneThird:
                    let horizontalSlices = segmentFrame.dividedIntegral(fraction: (2.0 / 3.0), from: .minXEdge)
                    let verticalSlices = horizontalSlices.second.dividedIntegral(fraction: 0.5, from: .minYEdge)
                    segmentRects = [horizontalSlices.first, verticalSlices.first, verticalSlices.second]
    
                case .oneThirdTwoThirds:
                    let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3.0), from: .minXEdge)
                    let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
                    segmentRects = [verticalSlices.first, verticalSlices.second, horizontalSlices.second]
                }
    
                // Create and cache layout attributes for calculated frames.
                for rect in segmentRects {
                    let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: currentIndex, section: 0))
                    attributes.frame = rect
    
                    cachedAttributes.append(attributes)
    

    // contentBounds = contentBounds.union(lastFrame) contentBounds = contentBounds.union(rect)

                    currentIndex += 1
                    lastFrame = rect
                }
    
    //            // Determine the next segment style.
    //            switch count - currentIndex {
    //            case 1:
    //                segment = .fullWidth
    //            case 2:
    //                segment = .fiftyFifty
    //            default:
    //                switch segment {
    //                case .fullWidth:
    //                    segment = .fiftyFifty
    //                case .fiftyFifty:
    //                    segment = .twoThirdsOneThird
    //                case .twoThirdsOneThird:
    //                    segment = .oneThirdTwoThirds
    //                case .oneThirdTwoThirds:
    //                    segment = .fiftyFifty
    //                }
    //            }
            }
        }
    
        /// - Tag: CollectionViewContentSize
        override var collectionViewContentSize: CGSize {
            return contentBounds.size
        }
    
        /// - Tag: ShouldInvalidateLayout
        override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
            guard let collectionView = collectionView else { return false }
            return !newBounds.size.equalTo(collectionView.bounds.size)
        }
    
        /// - Tag: LayoutAttributesForItem
        override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            return cachedAttributes[indexPath.item]
        }
    
        /// - Tag: LayoutAttributesForElements
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            var attributesArray = [UICollectionViewLayoutAttributes]()
    
            // Find any cell that sits within the query rect.
            guard let lastIndex = cachedAttributes.indices.last,
                  let firstMatchIndex = binSearch(rect, start: 0, end: lastIndex) else { return attributesArray }
    
            // Starting from the match, loop up and down through the array until all the attributes
            // have been added within the query rect.
            for attributes in cachedAttributes[..<firstMatchIndex].reversed() {
                guard attributes.frame.maxY >= rect.minY else { break }
                attributesArray.append(attributes)
            }
    
            for attributes in cachedAttributes[firstMatchIndex...] {
                guard attributes.frame.minY <= rect.maxY else { break }
                attributesArray.append(attributes)
            }
    
            return attributesArray
        }
    
        // Perform a binary search on the cached attributes array.
        func binSearch(_ rect: CGRect, start: Int, end: Int) -> Int? {
            if end < start { return nil }
    
            let mid = (start + end) / 2
            let attr = cachedAttributes[mid]
    
            if attr.frame.intersects(rect) {
                return mid
            } else {
                if attr.frame.maxY < rect.minY {
                    return binSearch(rect, start: (mid + 1), end: end)
                } else {
                    return binSearch(rect, start: start, end: (mid - 1))
                }
            }
        }
    }
    

    The divideIntegral extension:

    extension CGRect {
        func dividedIntegral(fraction: CGFloat, from fromEdge: CGRectEdge) -> (first: CGRect, second: CGRect) {
            let dimension: CGFloat
    
            switch fromEdge {
            case .minXEdge, .maxXEdge:
                dimension = self.size.width
            case .minYEdge, .maxYEdge:
                dimension = self.size.height
            }
    
            let distance = (dimension * fraction).rounded(.up)
            var slices = self.divided(atDistance: distance, from: fromEdge)
    
            switch fromEdge {
            case .minXEdge, .maxXEdge:
                slices.remainder.origin.x += 1
                slices.remainder.size.width -= 1
            case .minYEdge, .maxYEdge:
                slices.remainder.origin.y += 1
                slices.remainder.size.height -= 1
            }
    
            return (first: slices.slice, second: slices.remainder)
        }
    }
    

    The MosaicCell class:

    class MosaicCell: UICollectionViewCell {
        static let identifer = "kMosaicCollectionViewCell"
    
        var imageView = UIImageView()
        var assetIdentifier: String?
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            self.clipsToBounds = true
            self.autoresizesSubviews = true
    
            imageView.frame = self.bounds
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            self.addSubview(imageView)
    
            // Use a random background color.
            let redColor = CGFloat(arc4random_uniform(255)) / 255.0
            let greenColor = CGFloat(arc4random_uniform(255)) / 255.0
            let blueColor = CGFloat(arc4random_uniform(255)) / 255.0
            self.backgroundColor = UIColor(red: redColor, green: greenColor, blue: blueColor, alpha: 1.0)
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func prepareForReuse() {
            super.prepareForReuse()
            imageView.image = nil
            assetIdentifier = nil
        }
    }
    

    Your ViewController class:

    class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
    
        override func viewDidLoad() {
            let mosaicLayout = MosaicLayout()
            collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: mosaicLayout)
            collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            collectionView.alwaysBounceVertical = true
            collectionView.indicatorStyle = .white
            collectionView.delegate = self
            collectionView.dataSource = self
            collectionView.register(MosaicCell.self, forCellWithReuseIdentifier: MosaicCell.identifer)
        }
    
        override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return 3
        }
    
        override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MosaicCell.identifer, for: indexPath)
            cell.contentView.backgroundColor = .red
            return cell
        }
    }
    

    Here is the link to the Apple example: https://developer.apple.com/documentation/uikit/uicollectionview/customizing_collection_view_layouts

    You will need to configure later segments as desired.