UICollectionView Set number of columns UICollectionView Set number of columns ios ios

UICollectionView Set number of columns


With Swift 5 and iOS 12.3, you can use one the 4 following implementations in order to set the number of items per row in your UICollectionView while managing insets and size changes (including rotation).


#1. Subclassing UICollectionViewFlowLayout and using UICollectionViewFlowLayout's itemSize property

ColumnFlowLayout.swift:

import UIKitclass ColumnFlowLayout: UICollectionViewFlowLayout {    let cellsPerRow: Int    init(cellsPerRow: Int, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {        self.cellsPerRow = cellsPerRow        super.init()        self.minimumInteritemSpacing = minimumInteritemSpacing        self.minimumLineSpacing = minimumLineSpacing        self.sectionInset = sectionInset    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }    override func prepare() {        super.prepare()        guard let collectionView = collectionView else { return }        let marginsAndInsets = sectionInset.left + sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)        let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)        itemSize = CGSize(width: itemWidth, height: itemWidth)    }    override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {        let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext        context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size        return context    }}

CollectionViewController.swift:

import UIKitclass CollectionViewController: UICollectionViewController {    let columnLayout = ColumnFlowLayout(        cellsPerRow: 5,        minimumInteritemSpacing: 10,        minimumLineSpacing: 10,        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)    )    override func viewDidLoad() {        super.viewDidLoad()        collectionView?.collectionViewLayout = columnLayout        collectionView?.contentInsetAdjustmentBehavior = .always        collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")    }    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        return 59    }    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)        cell.backgroundColor = UIColor.orange        return cell    }}

enter image description here


#2. Using UICollectionViewFlowLayout's itemSize method

import UIKitclass CollectionViewController: UICollectionViewController {    let margin: CGFloat = 10    let cellsPerRow = 5    override func viewDidLoad() {        super.viewDidLoad()        guard let collectionView = collectionView, let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { return }        flowLayout.minimumInteritemSpacing = margin        flowLayout.minimumLineSpacing = margin        flowLayout.sectionInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)        collectionView.contentInsetAdjustmentBehavior = .always        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")    }    override func viewWillLayoutSubviews() {        guard let collectionView = collectionView, let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }        let marginsAndInsets = flowLayout.sectionInset.left + flowLayout.sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + flowLayout.minimumInteritemSpacing * CGFloat(cellsPerRow - 1)        let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)        flowLayout.itemSize =  CGSize(width: itemWidth, height: itemWidth)    }    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        return 59    }    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)        cell.backgroundColor = UIColor.orange        return cell    }    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {        collectionView?.collectionViewLayout.invalidateLayout()        super.viewWillTransition(to: size, with: coordinator)    }}

#3. Using UICollectionViewDelegateFlowLayout's collectionView(_:layout:sizeForItemAt:) method

import UIKitclass CollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {    let inset: CGFloat = 10    let minimumLineSpacing: CGFloat = 10    let minimumInteritemSpacing: CGFloat = 10    let cellsPerRow = 5    override func viewDidLoad() {        super.viewDidLoad()        collectionView?.contentInsetAdjustmentBehavior = .always        collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")    }    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {        return UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)    }    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {        return minimumLineSpacing    }    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {        return minimumInteritemSpacing    }    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {        let marginsAndInsets = inset * 2 + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)        let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)        return CGSize(width: itemWidth, height: itemWidth)    }    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        return 59    }    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)        cell.backgroundColor = UIColor.orange        return cell    }    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {        collectionView?.collectionViewLayout.invalidateLayout()        super.viewWillTransition(to: size, with: coordinator)    }}

#4. Subclassing UICollectionViewFlowLayout and using UICollectionViewFlowLayout's estimatedItemSize property

CollectionViewController.swift:

import UIKitclass CollectionViewController: UICollectionViewController {    let items = [        "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",        "Lorem ipsum dolor sit amet, consectetur.",        "Lorem ipsum dolor sit amet.",        "Lorem ipsum dolor sit amet, consectetur.",        "Lorem ipsum dolor sit amet, consectetur adipiscing.",        "Lorem ipsum.",        "Lorem ipsum dolor sit amet.",        "Lorem ipsum dolor sit.",        "Lorem ipsum dolor sit amet, consectetur adipiscing.",        "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.",        "Lorem ipsum dolor sit amet, consectetur."    ]    let columnLayout = FlowLayout(        cellsPerRow: 3,        minimumInteritemSpacing: 10,        minimumLineSpacing: 10,        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)    )    override func viewDidLoad() {        super.viewDidLoad()        collectionView?.collectionViewLayout = columnLayout        collectionView?.contentInsetAdjustmentBehavior = .always        collectionView?.register(Cell.self, forCellWithReuseIdentifier: "Cell")    }    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        return items.count    }    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell        cell.label.text = items[indexPath.row]        return cell    }    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {        collectionView?.collectionViewLayout.invalidateLayout()        super.viewWillTransition(to: size, with: coordinator)    }}

FlowLayout.swift:

import UIKitclass FlowLayout: UICollectionViewFlowLayout {    let cellsPerRow: Int    required init(cellsPerRow: Int = 1, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {        self.cellsPerRow = cellsPerRow        super.init()        self.minimumInteritemSpacing = minimumInteritemSpacing        self.minimumLineSpacing = minimumLineSpacing        self.sectionInset = sectionInset        estimatedItemSize = UICollectionViewFlowLayout.automaticSize    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else { return nil }        guard let collectionView = collectionView else { return layoutAttributes }        let marginsAndInsets = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + sectionInset.left + sectionInset.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)        layoutAttributes.bounds.size.width = ((collectionView.bounds.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)        return layoutAttributes    }    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {        let superLayoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }        guard scrollDirection == .vertical else { return superLayoutAttributes }        let layoutAttributes = superLayoutAttributes.compactMap { layoutAttribute in            return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute        }        // (optional) Uncomment to top align cells that are on the same line        /*        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {            guard let max = attributes.max(by: { $0.size.height < $1.size.height }) else { continue }            for attribute in attributes where attribute.size.height != max.size.height {                attribute.frame.origin.y = max.frame.origin.y            }        }         */        // (optional) Uncomment to bottom align cells that are on the same line        /*        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {            guard let max = attributes.max(by: { $0.size.height < $1.size.height }) else { continue }            for attribute in attributes where attribute.size.height != max.size.height {                attribute.frame.origin.y += max.frame.maxY - attribute.frame.maxY            }        }         */        return layoutAttributes    }}

Cell.swift:

import UIKitclass Cell: UICollectionViewCell {    let label = UILabel()    override init(frame: CGRect) {        super.init(frame: frame)        label.numberOfLines = 0        backgroundColor = .orange        contentView.addSubview(label)        label.translatesAutoresizingMaskIntoConstraints = false        label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor).isActive = true        label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true        label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true        label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {        layoutIfNeeded()        label.preferredMaxLayoutWidth = label.bounds.size.width        layoutAttributes.bounds.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height        return layoutAttributes    }    // Alternative implementation    /*    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {        label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.right        layoutAttributes.bounds.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height        return layoutAttributes    }    */}

enter image description here


CollectionViews are very powerful, and they come at a price. Lots, and lots of options. As omz said:

there are multiple ways you could change the number of columns

I'd suggest implementing the <UICollectionViewDelegateFlowLayout> Protocol, giving you access to the following methods in which you can have greater control over the layout of your UICollectionView, without the need for subclassing it:

  • collectionView:layout:insetForSectionAtIndex:
  • collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
  • collectionView:layout:minimumLineSpacingForSectionAtIndex:
  • collectionView:layout:referenceSizeForFooterInSection:
  • collectionView:layout:referenceSizeForHeaderInSection:
  • collectionView:layout:sizeForItemAtIndexPath:

Also, implementing the following method will force your UICollectionView to update it's layout on an orientation change: (say you wanted to re-size the cells for landscape and make them stretch)

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation                               duration:(NSTimeInterval)duration{    [self.myCollectionView.collectionViewLayout invalidateLayout];}

Additionally, here are 2 really good tutorials on UICollectionViews:

http://www.raywenderlich.com/22324/beginning-uicollectionview-in-ios-6-part-12

http://skeuo.com/uicollectionview-custom-layout-tutorial


I implemented UICollectionViewDelegateFlowLayout on my UICollectionViewController and override the method responsible for determining the size of the Cell. I then took the screen width and divided it with my column requirement. For example, I wanted to have 3 columns on each screen size. So here's what my code looks like -

- (CGSize)collectionView:(UICollectionView *)collectionView                  layout:(UICollectionViewLayout *)collectionViewLayout  sizeForItemAtIndexPath:(NSIndexPath *)indexPath{    CGRect screenRect = [[UIScreen mainScreen] bounds];    CGFloat screenWidth = screenRect.size.width;    float cellWidth = screenWidth / 3.0; //Replace the divisor with the column count requirement. Make sure to have it in float.    CGSize size = CGSizeMake(cellWidth, cellWidth);    return size;}