RaptoX
RaptoX

Reputation: 2593

How to center horizontally UICollectionView Cells?

I have done some research, but I couldn't find any code example on how to center cells in a UICollectionView horizontally.

instead of the first cell being like this X00, I want it to be like this 0X0. is there any way to accomplish this?

EDIT:

to visualize what I want:

enter image description here

I need it to look like version B when there is only one element in the CollectionView. When I got more than one element, then it should be like version A but with more elements.

At the moment it looks like Version A when I have only 1 element, and I wonder how I can make it look like B.

Thanks for the help!

Upvotes: 173

Views: 150027

Answers (30)

clearlight
clearlight

Reputation: 12615

Assuming the collectionView is constrained to the bounds of a view and that the collectionView's content can be contained within the width of the view...

    NSLayoutConstraint.activate([
       collectionView.topAnchor.constraint(equalTo: view.topAnchor),
       collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
       collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
       collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    ]

You could try something like this where you reload your collectionView:

    collectionView?.reload()
    let contentSize = collectionView.collectionViewLayout.collectionViewContentSize
    let space = view.bounds.size.width - contentSize.width
    collectionView.contentInset = UIEdgeInsets(top: 0, left: space / 2, bottom: 0, right: space / 2)

Alternatively, you could main a reference (variable) to the collectionView.leadingAnchor.constraint, and just change that constraint's offset to space / 2 after reloading the table and that would keep the collectionView content from scrolling horizontally, for example if you'd set the layout scrollDirection to .horizontal

Upvotes: 0

Brotsky Engineer
Brotsky Engineer

Reputation: 41

Change cellWidth and paddingInside

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        let cellWidth : CGFloat = 100
        let paddingInside: CGFloat = 10
        
        let numberOfCells = floor(collectionView.frame.size.width / cellWidth)
        let edgeInsets = (collectionView.frame.size.width - (numberOfCells * cellWidth)) / 2 - paddingInside
        
        
        return UIEdgeInsets(top: 0, left: edgeInsets, bottom: 0, right: edgeInsets)
    }

and do no forget confirm your vc to UICollectionViewDelegateFlowLayout protocol

Upvotes: 1

Set insets by collectionView content size. Below i have used cell width by its text length. collectionView center and cell size according to text

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            let font = FontNames.mediumFont12
            let fontAttributes = [NSAttributedString.Key.font: font]
            let text = "Your text"
            let size = (text as NSString).size(withAttributes: fontAttributes)
            self.setInsets(width: self.tagsCollectionView.contentSize.width)
            return CGSize(width: size.width+30.0, height: self.tagsCollectionView.bounds.height)
}
func setInsets(width: CGFloat) {
            let inset = self.tagsCollectionView.bounds.width - width
            if inset <= 26.0 {
                self.tagsCollectionView.contentInset = UIEdgeInsets(top: 0.0, left: 26.0, bottom: 0.0, right: 26.0)
            } else {
                self.tagsCollectionView.contentInset = UIEdgeInsets(top: 0.0, left: inset/2.0, bottom: 0.0, right: inset/2.0)
            }
        }

Upvotes: 0

Pavel Paddubotski
Pavel Paddubotski

Reputation: 127

I've dealed with the same problem and as a solution I've copied layout from this project and applied it to my UICollectionView as a UICollectionViewFlowLayout. You don't need apply the full library, just copy layout class.

https://github.com/Coeur/CollectionViewCenteredFlowLayout

Upvotes: 1

budiDino
budiDino

Reputation: 13527

Swift 5 modification of the currently accepted answer that fixes the issues when your cells can't fit the screen width.

Make sure to replace cellWidth and cellSpacing with your own custom values.

func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        insetForSectionAt section: Int) -> UIEdgeInsets {
    let cellCount = collectionView.numberOfItems(inSection: section)
    let totalCellWidth = cellWidth * CGFloat(cellCount)
    let totalSpacingWidth = cellSpacing * CGFloat(cellCount - 1)
    let collectionViewInset = collectionView.contentInset.left + collectionView.contentInset.right

    let inset = (collectionView.frame.width - collectionViewInset - totalCellWidth - totalSpacingWidth) / 2

    return inset > 0
        ? UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
        : UIEdgeInsets.zero
}

Upvotes: 4

Yaron Abramovich
Yaron Abramovich

Reputation: 91

In multiple rows, this worked for me:

    func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    insetForSectionAt section: Int) -> UIEdgeInsets {

    let cellWidth = 370
    let cellSpaceing = 10
    let totalCellWithSpace = cellWidth + cellSpaceing
    let howManyCellsInRow = Int(Int(collectionView.layer.frame.size.width) / (totalCellWithSpace))
    let inset = (collectionView.layer.frame.size.width - CGFloat(howManyCellsInRow * totalCellWithSpace)) / 2
    return UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)

}

Upvotes: 0

Mehdi S.
Mehdi S.

Reputation: 471

another approach is to divide the space you have by the number of cells you want to display, this way you will mimic the .FillEqually mode of stackViews

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: collectionView.frame.width / CGFloat(collectionView.numberOfItems(inSection: indexPath.section)), height: 28)
}

don't forget to :

  • set the view in the cell contentview to a fixed width, centered in it's container without trailing and leading constraint
  • remove any minimum spacing for cells and lines in the inspector of the CollectionView, set them to 0 each
  • provide a height in the snippet of code above, instead of 28 as it is right now

Upvotes: 0

Wimukthi Rajapaksha
Wimukthi Rajapaksha

Reputation: 1019

You have to define a custom UICollectionViewFlowLayout here as bellow.

class CenterAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let superArray = super.layoutAttributesForElements(in: rect) else {  return nil  }
        guard let attributes = NSArray(array: superArray, copyItems: true) as? [UICollectionViewLayoutAttributes] else {  return nil  }
        // Constants
        let leftPadding: CGFloat = 8
        let interItemSpacing = minimumInteritemSpacing
        // Tracking values
        var leftMargin: CGFloat = leftPadding // Modified to determine origin.x for each item
        var maxY: CGFloat = -1.0 // Modified to determine origin.y for each item
        var rowSizes: [[CGFloat]] = [] // Tracks the starting and ending x-values for the first and last item in the row
        var currentRow: Int = 0 // Tracks the current row
        attributes.forEach { layoutAttribute in
            // Each layoutAttribute represents its own item
            if layoutAttribute.frame.origin.y >= maxY {
                // This layoutAttribute represents the left-most item in the row
                leftMargin = leftPadding
                // Register its origin.x in rowSizes for use later
                if rowSizes.count == 0 {
                    // Add to first row
                    rowSizes = [[leftMargin, 0]]
                } else {
                    // Append a new row
                    rowSizes.append([leftMargin, 0])
                    currentRow += 1
                }
            }
            layoutAttribute.frame.origin.x = leftMargin
            leftMargin += layoutAttribute.frame.width + interItemSpacing
            maxY = max(layoutAttribute.frame.maxY, maxY)
            // Add right-most x value for last item in the row
            rowSizes[currentRow][1] = leftMargin - interItemSpacing
        }
        // At this point, all cells are left aligned
        // Reset tracking values and add extra left padding to center align entire row
        leftMargin = leftPadding
        maxY = -1.0
        currentRow = 0
        attributes.forEach { layoutAttribute in
            // Each layoutAttribute is its own item
            if layoutAttribute.frame.origin.y >= maxY {
                // This layoutAttribute represents the left-most item in the row
                leftMargin = leftPadding
                // Need to bump it up by an appended margin
                let rowWidth = rowSizes[currentRow][1] - rowSizes[currentRow][0] // last.x - first.x
                let appendedMargin = (collectionView!.frame.width - leftPadding  - rowWidth - leftPadding) / 2
                leftMargin += appendedMargin
                currentRow += 1
            }
            layoutAttribute.frame.origin.x = leftMargin
            leftMargin += layoutAttribute.frame.width + interItemSpacing
            maxY = max(layoutAttribute.frame.maxY, maxY)
        }
        return attributes
    }
}

Then add following to the relevant viewController's viewDidLoad or relevant method,

let layout = CenterAlignedCollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: 200, height: 40)
self.collectionRateCustomer.collectionViewLayout = layout

Don't forget to inherit UICollectionViewDelegateFlowLayout

References, equaleyes.com and Alex Koshy's answer

Upvotes: 0

user11051170
user11051170

Reputation:

If you're using Darshan Patel's answer, you can also perform the calculation inside UICollectionViewFlowLayout subclass.

class Layout: UICollectionViewFlowLayout {

    override init() {
        super.init()
        scrollDirection = .horizontal
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError()
    }

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView,
            collectionView.numberOfSections != 0 else { return }
    
        minimumInteritemSpacing = 5
        itemSize = CGSize(width: 30, height: 30)

        sectionInsetReference = .fromSafeArea

        let numberOfItems = collectionView.numberOfItems(inSection: 0)

        let totalCellWidth = itemSize.width * CGFloat(numberOfItems)
        let totalSpacingWidth = minimumInteritemSpacing * CGFloat(numberOfItems - 1)

        let leftInset = (collectionView.bounds.maxX - CGFloat(totalCellWidth + totalSpacingWidth)) / 2

        sectionInset = UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: leftInset)
    }
}

Upvotes: 2

chris
chris

Reputation: 81

For swift 5.2

I made some adjustments to meet my requirements

  1. Not to scroll if the content didn't extend outside the screen
  2. Center if the content didn't extend outside the screen
  3. If the content did extend outside the screen make sure to start at index 0 and allow scrolling while indenting from the edge by 16 (easy to adjust)
func centerItemsInCollectionView(cellWidth: Double, numberOfItems: Double, spaceBetweenCell: Double, collectionView: UICollectionView) -> UIEdgeInsets {
        let totalWidth = cellWidth * numberOfItems
        let totalSpacingWidth = spaceBetweenCell * (numberOfItems - 1)
        let leftInset = (collectionView.frame.width - CGFloat(totalWidth + totalSpacingWidth)) / 2
        let rightInset = leftInset
        if leftInset < 0 {
            collectionView.isScrollEnabled = true
            return UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
        }
        collectionView.isScrollEnabled = false
        return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
    }

Use this by doing this in your view controller

extension BaseViewController : UICollectionViewDelegateFlowLayout {

then you can call

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return centerItemsInCollectionView(cellWidth: 70, numberOfItems: 3, spaceBetweenCell: 25, collectionView: collectionView)
}

keep in mind if you want to set your own size for your cells you need to set the collectionView Estimated Size to none in storyboard and set your cells size to custom in storyboard

then you can call

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize.init(width:60, height: 60)
}

if you are struggling to get methods to get called it's good to get all your delegates listened for

extension BaseViewController : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

and obviously don't forget to set your delegates, simply extending them won't do the trick.

override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.delegate = self
        collectionView.dataSource = self
...

Upvotes: -1

Ashish Gupta
Ashish Gupta

Reputation: 378

There is one improvement in the above answer for when the data is more; Use following code:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
      var totalCellsWidth:CGFloat = 0
        for index in 0..<numberOfCells{
            let width = viewModel.getCellWidth(at: index) // get cell width is to dynamically calculate the width of the cell according to the text
            totalCellsWidth += width
        }
        let cellSpacing:CGFloat = 8.0
        let totalSpacingWidth = cellSpacing * CGFloat(viewModel.teamsCount - 1)
       
        var leftInset = (collectionView.frame.width - (totalCellsWidth + totalSpacingWidth)) / 2
        if leftInset < 0{
            leftInset = 0
        }
        let rightInset = leftInset
        return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}

Upvotes: 0

oscar castellon
oscar castellon

Reputation: 3138

Try this for Swift 4

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        let cellWidth : CGFloat = 165.0
        
        let numberOfCells = floor(collectionView.frame.size.width / cellWidth)
        let edgeInsets = (collectionView.frame.size.width - (numberOfCells * cellWidth)) / (numberOfCells + 1)
        
        return UIEdgeInsetsMake(15, edgeInsets, 0, edgeInsets)
    }

Add your cellWidth instead 165.0

Upvotes: 30

Ahmed Safadi
Ahmed Safadi

Reputation: 4590

Swift 5.1

func centerItemsInCollectionView(cellWidth: Double, numberOfItems: Double, spaceBetweenCell: Double, collectionView: UICollectionView) -> UIEdgeInsets {
    let totalWidth = cellWidth * numberOfItems
    let totalSpacingWidth = spaceBetweenCell * (numberOfItems - 1)
    let leftInset = (collectionView.frame.width - CGFloat(totalWidth + totalSpacingWidth)) / 2
    let rightInset = leftInset
    return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}

Swift 4.2

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {

    let totalCellWidth = 80 * collectionView.numberOfItems(inSection: 0)
    let totalSpacingWidth = 10 * (collectionView.numberOfItems(inSection: 0) - 1)

    let leftInset = (collectionView.layer.frame.size.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
    let rightInset = leftInset

    return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)

}

Swift 3

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {

        let totalCellWidth = 80 * collectionView.numberOfItems(inSection: 0)
        let totalSpacingWidth = 10 * (collectionView.numberOfItems(inSection: 0) - 1)

        let leftInset = (collectionView.layer.frame.size.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
        let rightInset = leftInset

        return UIEdgeInsetsMake(0, leftInset, 0, rightInset)

    }

don't forget to add the protocol

UICollectionViewDelegateFlowLayout

Upvotes: 86

doroboneko
doroboneko

Reputation: 1

I used that code in a project. It centers the collectionView horizontally and vertically in both direction .horizontal and .vertical using the insets of section. Respects the spacing and the original inset of the section if set. Code to use in the delegate UICollectionViewDelegateFlowLayout so we have access to all the properties we need to retrieve from the UIcollectionView or set in the storyboard for reusability.

// original function of the delegate
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    // casting the layout as a UICollectionViewFlowLayout to have access to the properties of items for reusability - you could also link the real one from the storyboard with an outlet
    let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
    // getting all the properties we need
    let itemWidth = flowLayout.itemSize.width
    let itemHeight = flowLayout.itemSize.height
    let interSpacing = flowLayout.minimumInteritemSpacing
    let lineSpacing = flowLayout.minimumLineSpacing
    // getting the size of the collectionView
    let collectionWidth = collectionView.bounds.width
    let collectionHeight = collectionView.bounds.height
    // getting the direction to choose how to align the collection
    let direction = flowLayout.scrollDirection
    // you don't want to have an item greater than the collection
    guard (itemWidth < collectionWidth && direction == .vertical) || (itemHeight < collectionHeight && direction == .horizontal) else {
        print("Really?")
        return UIEdgeInsets(top: flowLayout.sectionInset.top, left: flowLayout.sectionInset.left, bottom: flowLayout.sectionInset.bottom, right: flowLayout.sectionInset.right)
    }
    // getting the number of item in the current section
    let totalItemCount = CGFloat(collectionView.numberOfItems(inSection: section))
    // setting number of item in a row to the max number of items that can fit in a row without spacing or to the number of items in the section if less than the max
    var itemCountInRow = totalItemCount < (collectionWidth / itemWidth).rounded(.towardZero) ? totalItemCount : (collectionWidth / itemWidth).rounded(.towardZero)
    // how many max row can we have
    var countOfRow = totalItemCount < (collectionHeight / itemHeight).rounded(.towardZero) ? totalItemCount : (collectionHeight / itemHeight).rounded(.towardZero)
    // calculating the total width of row by multiplying the number of items in the row by the width of item and adding the spacing multiplied by the number of item minus one
    var totalWidthOfRow:CGFloat {
        get{
            return (itemWidth * itemCountInRow) + (interSpacing * (itemCountInRow - 1))
        }
    }
    // calculating the total height of row by multiplying the number of row by the height of item and adding the spacing multiplied by the number of row minus one
    var totalHeightOfRow:CGFloat {
        get{
            return (itemHeight * countOfRow) + (lineSpacing * (countOfRow - 1))
        }
    }
    // first we set the inset to the default
    var edgeInsetLeft = flowLayout.sectionInset.left
    var edgeInsetTop = flowLayout.sectionInset.top

    if direction == .vertical {
        // while the width of row with original margin is greater than the width of the collection we drop one item until it fits
        while totalWidthOfRow > collectionWidth || ((collectionWidth - totalWidthOfRow) / 2) < flowLayout.sectionInset.left {
            // droping an item to fit in the row
            itemCountInRow -= 1
        }
        // calculating the number of rows in collectionView by dividing the number of items by the number of items in a row
        countOfRow = (totalItemCount / (itemCountInRow)).rounded(.up)
    } else {
        itemCountInRow = (totalItemCount / countOfRow).rounded(.up)
        // while the height of row with original marginis greater than the height of the collection we drop one row until it fits
        while totalHeightOfRow >= collectionHeight  || ((collectionHeight - totalHeightOfRow) / 2) < flowLayout.sectionInset.top  {
            // droping an item to fit in the row
            countOfRow -= 1
        }
    }
    edgeInsetLeft = max(flowLayout.sectionInset.left, (collectionWidth - totalWidthOfRow) / 2)
    edgeInsetTop = max(flowLayout.sectionInset.top, (collectionHeight - totalHeightOfRow) / 2)
    // we don't specially need insets where the items are overflowing
    let edgeInsetRight = direction == .vertical ? edgeInsetLeft : flowLayout.sectionInset.right
    let edgeInsetBottom = direction == .horizontal ? edgeInsetTop : flowLayout.sectionInset.bottom
    // returning the UIEdgeInsets
    return UIEdgeInsets(top: edgeInsetTop, left: edgeInsetLeft, bottom: edgeInsetBottom, right: edgeInsetRight)
}

Hope it would help someone - it centers the section not the items inside the section, for more we have to subclass the UICollectionViewFlowLayout or UICollectionViewLayout as the mosaic example from Apple.

Upvotes: 0

Bruno
Bruno

Reputation: 11

If there is only room for one cell per group, a leading: and trailing: of .flexible(0) will center the cell horizontally:

item.edgeSpacing = NSCollectionLayoutEdgeSpacing(
    leading: .flexible(0), top: nil,                                                     
    trailing: .flexible(0), bottom: nil
)

Upvotes: 1

remykits
remykits

Reputation: 1775

the simplest way is to set collection view estimate size to None in storyboard or with code layout.estimatedItemSize = CGSize.zero

Upvotes: 0

Raj
Raj

Reputation: 6015

I have similar situation in project, and I fixed it by referring UPCarouselFlowLayout

I think it support swift 5 version

https://github.com/ink-spot/UPCarouselFlowLayout/blob/master/UPCarouselFlowLayout/UPCarouselFlowLayout.swift

Look at the code implementation in

override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint

Upvotes: 0

Saeed Ir
Saeed Ir

Reputation: 2332

Here is the newer version for Swift 5 which also works fine when the cells are more than one row:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
    let cellWidth: CGFloat = flowLayout.itemSize.width
    let cellSpacing: CGFloat = flowLayout.minimumInteritemSpacing
    var cellCount = CGFloat(collectionView.numberOfItems(inSection: section))
    var collectionWidth = collectionView.frame.size.width
    var totalWidth: CGFloat
    if #available(iOS 11.0, *) {
        collectionWidth -= collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right
    }
    repeat {
        totalWidth = cellWidth * cellCount + cellSpacing * (cellCount - 1)
        cellCount -= 1
    } while totalWidth >= collectionWidth

    if (totalWidth > 0) {
        let edgeInset = (collectionWidth - totalWidth) / 2
        return UIEdgeInsets.init(top: flowLayout.sectionInset.top, left: edgeInset, bottom: flowLayout.sectionInset.bottom, right: edgeInset)
    } else {
        return flowLayout.sectionInset
    }
}

Please make sure your class conforms to UICollectionViewDelegateFlowLayout protocol.

Upvotes: 9

Darshan Patel.
Darshan Patel.

Reputation: 3162

Its not a good idea to use a library, if your purpose is only this i.e to centre align.

Better you can do this simple calculation in your collectionViewLayout function.

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {

    let totalCellWidth = CellWidth * CellCount
    let totalSpacingWidth = CellSpacing * (CellCount - 1)

    let leftInset = (collectionViewWidth - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
    let rightInset = leftInset

    return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}

Upvotes: 301

Peter Lapisu
Peter Lapisu

Reputation: 20965

General solution for flowlayout that centers the pages if they are less than the width and aligns left if there are more

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(nonnull UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
    // Centering if there are fever pages
    CGSize itemSize = [(UICollectionViewFlowLayout *)collectionViewLayout itemSize];
    CGFloat spacing = [(UICollectionViewFlowLayout *)collectionViewLayout minimumLineSpacing];

    NSInteger count = [self collectionView:self numberOfItemsInSection:section];
    CGFloat totalCellWidth = itemSize.width * count;
    CGFloat totalSpacingWidth = spacing * ((count - 1) < 0 ? 0 : count - 1);
    CGFloat leftInset = (self.bounds.size.width - (totalCellWidth + totalSpacingWidth)) / 2;
    if (leftInset < 0) {
        UIEdgeInsets inset = [(UICollectionViewFlowLayout *)collectionViewLayout sectionInset];
        return inset;
    }
    CGFloat rightInset = leftInset;
    UIEdgeInsets sectionInset = UIEdgeInsetsMake(0, leftInset, 0, rightInset);
    return sectionInset;
}

Swift version (converted from ObjC)

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    // Centering if there are fever pages
    let itemSize: CGSize? = (collectionViewLayout as? UICollectionViewFlowLayout)?.itemSize
    let spacing: CGFloat? = (collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing

    let count: Int = self.collectionView(self, numberOfItemsInSection: section)
    let totalCellWidth = (itemSize?.width ?? 0.0) * CGFloat(count)
    let totalSpacingWidth = (spacing ?? 0.0) * CGFloat(((count - 1) < 0 ? 0 : count - 1))
    let leftInset: CGFloat = (bounds.size.width - (totalCellWidth + totalSpacingWidth)) / 2
    if leftInset < 0 {
        let inset: UIEdgeInsets? = (collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset
        return inset!
    }
    let rightInset: CGFloat = leftInset
    let sectionInset = UIEdgeInsets(top: 0, left: Float(leftInset), bottom: 0, right: Float(rightInset))
    return sectionInset
}

Untitled-3.png

Upvotes: 9

Brooks DuBois
Brooks DuBois

Reputation: 725

I ended up taking a completely different approach here, which I believe is worth mentioning.

I set a constraint on my collection view to be horizontally aligned in the center. Then I set another constraint that specifies the width. I created an outlet for the width constraint inside of my viewController that holds the collection view. Then, when my data source is changed and I am updating the collection view, I take the count of the cells and do a (very similar) calculation to reset the width.

let newWidth = (items.count * cellWidth) + (items.count * cellSpacing)

Then I set the constraint outlet's .constant value to the calculation result and autolayout does the rest.

This may conflict with the `UICollectionViewDelegateFlowLayout, but this worked perfectly for me to create a left-justified collection view. Without a delegate, it only seems to work when the cells filled up the majority of the view.

Upvotes: 0

EVGENIY DANILOV
EVGENIY DANILOV

Reputation: 91

Swift 4.2 (Horizontally and Vertically). It's small upgrade of Pink Panther code and big thanks him!


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
    let cellWidth: CGFloat = flowLayout.itemSize.width
    let cellHieght: CGFloat = flowLayout.itemSize.height
    let cellSpacing: CGFloat = flowLayout.minimumInteritemSpacing
    let cellCount = CGFloat(collectionView.numberOfItems(inSection: section))
    var collectionWidth = collectionView.frame.size.width
    var collectionHeight = collectionView.frame.size.height
    if #available(iOS 11.0, *) {
        collectionWidth -= collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right
        collectionHeight -= collectionView.safeAreaInsets.top + collectionView.safeAreaInsets.bottom
    }
    let totalWidth = cellWidth * cellCount + cellSpacing * (cellCount - 1)
    let totalHieght = cellHieght * cellCount + cellSpacing * (cellCount - 1)
    if totalWidth <= collectionWidth {
        let edgeInsetWidth = (collectionWidth - totalWidth) / 2

        print(edgeInsetWidth, edgeInsetWidth)
        return UIEdgeInsets(top: 5, left: edgeInsetWidth, bottom: flowLayout.sectionInset.top, right: edgeInsetWidth)
    } else {
        let edgeInsetHieght = (collectionHeight - totalHieght) / 2
        print(edgeInsetHieght, edgeInsetHieght)
        return UIEdgeInsets(top: edgeInsetHieght, left: flowLayout.sectionInset.top, bottom: edgeInsetHieght, right: flowLayout.sectionInset.top)

    }
}

Make sure your class conforms to UICollectionViewDelegateFlowLayout protocol

Upvotes: 5

Saeed Ir
Saeed Ir

Reputation: 2332

This code should center horizontally collection view even in Swift 4.0 without any modification:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
    let cellWidth: CGFloat = flowLayout.itemSize.width
    let cellSpacing: CGFloat = flowLayout.minimumInteritemSpacing
    let cellCount = CGFloat(collectionView.numberOfItems(inSection: section))
    var collectionWidth = collectionView.frame.size.width
    if #available(iOS 11.0, *) {
        collectionWidth -= collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right
    }
    let totalWidth = cellWidth * cellCount + cellSpacing * (cellCount - 1)
    if totalWidth <= collectionWidth {
        let edgeInset = (collectionWidth - totalWidth) / 2
        return UIEdgeInsetsMake(flowLayout.sectionInset.top, edgeInset, flowLayout.sectionInset.bottom, edgeInset)
    } else {
        return flowLayout.sectionInset
    }
}

Make sure your class conforms to UICollectionViewDelegateFlowLayout protocol

Upvotes: 1

iEvgen Podkorytov
iEvgen Podkorytov

Reputation: 776

SWIFT 4.2

private lazy var contentView: UICollectionView = {
        let layoutView: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
            layoutView.scrollDirection = .horizontal
            layoutView.minimumInteritemSpacing = 0
            layoutView.minimumLineSpacing = 5

        let collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: layoutView)
            collectionView.dataSource = self
            collectionView.delegate = self
            collectionView.showsHorizontalScrollIndicator = false
            collectionView.isPagingEnabled = true
            collectionView.registerCell(Cell.self)
            collectionView.backgroundColor = .clear
            collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()

//

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

        return CGSize(width: collectionView.frame.width*4/5, height: collectionView.frame.height)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        let cellWidth : CGFloat = collectionView.frame.width*4/5

        let numberOfCells = floor(collectionView.frame.width / cellWidth)
        let edgeInsets = (collectionView.frame.width - (numberOfCells * cellWidth)) / (numberOfCells + 1)

        return UIEdgeInsets(top: 0, left: edgeInsets, bottom: 0, right: edgeInsets)
    }
}

Upvotes: 2

chainstair
chainstair

Reputation: 817

For the people who only want to add a padding (top, left, bottom, right):

Add the protocol UICollectionViewDelegateFlowLayout

This example shows a padding left and right with 40.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {        
    return UIEdgeInsetsMake(0, 40, 0, 40)
}

Upvotes: 3

azwethinkweiz
azwethinkweiz

Reputation: 592

The accepted answer is the right answer but if your totalCellWidth is less than the CollectionView's width, but just to guard against this you can do as below.

if (leftInset > 0) {
     return UIEdgeInsetsMake(0, leftInset, 0, rightInset)
  } else {
     return UIEdgeInsetsMake(0, 10, 0, 10)
}

Upvotes: 1

S. Matsepura
S. Matsepura

Reputation: 1693

You can use this extension (Swift 4).

It can center cells with if you collectionView have layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize.

It work with any cells size and work perfectly when scrollDirection = .horizontal

public extension UICollectionView {
    func centerContentHorizontalyByInsetIfNeeded(minimumInset: UIEdgeInsets) {
        guard let layout = collectionViewLayout as? UICollectionViewFlowLayout,
            layout.scrollDirection == .horizontal else {
                assertionFailure("\(#function): layout.scrollDirection != .horizontal")
                return
        }

        if layout.collectionViewContentSize.width > frame.size.width {
            contentInset = minimumInset
        } else {
            contentInset = UIEdgeInsets(top: minimumInset.top,
                                        left: (frame.size.width - layout.collectionViewContentSize.width) / 2,
                                        bottom: minimumInset.bottom,
                                        right: 0)
        }
    }
}


final class Foo: UIViewController {
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        collectionView.centerContentHorizontalyByInsetIfNeeded(minimumInset: yourDefaultInset)
    }
}

Hope it's help you!

Upvotes: 9

Haroldo Gondim
Haroldo Gondim

Reputation: 7995

Swift 4

extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {

        let cellWidth: CGFloat = 170.0 // Your cell width

        let numberOfCells = floor(view.frame.size.width / cellWidth)
        let edgeInsets = (view.frame.size.width - (numberOfCells * cellWidth)) / (numberOfCells + 1)

        return UIEdgeInsetsMake(0, edgeInsets, 0, edgeInsets)
    }

 }

Upvotes: 3

Stunner
Stunner

Reputation: 12194

An objective-C version of Darshan Patel's answer:

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(nonnull UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
    CGFloat totalCellWidth = kItemWidth * self.dataArray.count;
    CGFloat totalSpacingWidth = kSpacing * (((float)self.dataArray.count - 1) < 0 ? 0 :self.dataArray.count - 1);
    CGFloat leftInset = (self.bounds.size.width - (totalCellWidth + totalSpacingWidth)) / 2;
    CGFloat rightInset = leftInset;
    UIEdgeInsets sectionInset = UIEdgeInsetsMake(0, leftInset, 0, rightInset);
    return sectionInset;
}

Upvotes: 19

MVZ
MVZ

Reputation: 2250

Slightly modifying @Safad Funy's answer, this is what worked for me in the lattest version of Swift & iOS. In this case I wanted the cells's width to be a third of the size of the collection view.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {

  let totalCellWidth = Int(collectionView.layer.frame.size.width) / 3 * collectionView.numberOfItems(inSection: 0)
  let totalSpacingWidth = (collectionView.numberOfItems(inSection: 0) - 1)

  let leftInset = (collectionView.layer.frame.size.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
  let rightInset = leftInset

  return UIEdgeInsetsMake(0, leftInset, 0, rightInset)
}

Upvotes: 7

Related Questions