Reputation: 2593
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:
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
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
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
Reputation: 51
Set insets by collectionView content size. Below i have used cell width by its text length.
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
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
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
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
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 :
Upvotes: 0
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
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
Reputation: 81
For swift 5.2
I made some adjustments to meet my requirements
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
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
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
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
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
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
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
Reputation: 6015
I have similar situation in project, and I fixed it by referring UPCarouselFlowLayout
I think it support swift 5
version
Look at the code implementation in
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint
Upvotes: 0
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
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
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
}
Upvotes: 9
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
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
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
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
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
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
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
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
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
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