Left Align Cells in UICollectionView - ios

I am using a UICollectionView in my project, where there are multiple cells of differing widths on a line. According to:
https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html
it spreads the cells out across the line with equal padding. This happens as expected, except I want to left justify them, and hard code a padding width.
I figure I need to subclass UICollectionViewFlowLayout, however after reading some of the tutorials etc online I just don't seem to get how this works.

The other solutions in this thread do not work properly, when the line is composed by only 1 item or are over complicated.
Based on the example given by Ryan, I changed the code to detect a new line by inspecting the Y position of the new element. Very simple and quick in performance.
Swift:
class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.y >= maxY {
leftMargin = sectionInset.left
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
If you want to have supplementary views keep their size, add the following at the top of the closure in the forEach call:
guard layoutAttribute.representedElementCategory == .cell else {
return
}
Objective-C:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
CGFloat maxY = -1.0f;
//this loop assumes attributes are in IndexPath order
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if (attribute.frame.origin.y >= maxY) {
leftMargin = self.sectionInset.left;
}
attribute.frame = CGRectMake(leftMargin, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);
leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing;
maxY = MAX(CGRectGetMaxY(attribute.frame), maxY);
}
return attributes;
}

There are many great ideas included in the answers to this question. However, most of them have some drawbacks:
Solutions that don't check the cell's y value only work for single-line layouts. They fail for collection view layouts with multiple lines.
Solutions that do check the y value like Angel García Olloqui's answer only work if all cells have the same height. They fail for cells with a variable height.
Most solutions only override the layoutAttributesForElements(in rect: CGRect) function. They do not override layoutAttributesForItem(at indexPath: IndexPath). This is a problem because the collection view periodically calls the latter function to retrieve the layout attributes for a particular index path. If you don't return the proper attributes from that function, you're likely to run into all sort of visual bugs, e.g. during insertion and deletion animations of cells or when using self-sizing cells by setting the collection view layout's estimatedItemSize.
The Apple docs state:
Every custom layout object is expected to implement the layoutAttributesForItemAtIndexPath: method.
Many solutions also make assumptions about the rect parameter that is passed to the layoutAttributesForElements(in rect: CGRect) function. For example, many are based on the assumption that the rect always starts at the beginning of a new line which is not necessarily the case.
So in other words:
Most of the solutions suggested on this page work for some specific applications, but they don't work as expected in every situation.
AlignedCollectionViewFlowLayout
In order to address these issues I've created a UICollectionViewFlowLayout subclass that follows a similar idea as suggested by matt and Chris Wagner in their answers to a similar question. It can either align the cells
⬅︎ left:
or ➡︎ right:
and additionally offers options to vertically align the cells in their respective rows (in case they vary in height).
You can simply download it here:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout
The usage is straight-forward and explained in the README file. You basically create an instance of AlignedCollectionViewFlowLayout, specify the desired alignment and assign it to your collection view's collectionViewLayout property:
let alignedFlowLayout = AlignedCollectionViewFlowLayout(
horizontalAlignment: .left,
verticalAlignment: .top
)
yourCollectionView.collectionViewLayout = alignedFlowLayout
(It's also available on Cocoapods.)
How it works (for left-aligned cells):
The concept here is to rely solely on the layoutAttributesForItem(at indexPath: IndexPath) function. In the layoutAttributesForElements(in rect: CGRect) we simply get the index paths of all cells within the rect and then call the first function for every index path to retrieve the correct frames:
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// We may not change the original layout attributes
// or UICollectionViewFlowLayout might complain.
let layoutAttributesObjects = copy(
super.layoutAttributesForElements(in: rect)
)
layoutAttributesObjects?.forEach({ (layoutAttributes) in
if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
let indexPath = layoutAttributes.indexPath
// Retrieve the correct frame from layoutAttributesForItem(at: indexPath):
if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
layoutAttributes.frame = newFrame
}
}
})
return layoutAttributesObjects
}
(The copy() function simply creates a deep copy of all layout attributes in the array. You may look into the source code for its implementation.)
So now the only thing we have to do is to implement the layoutAttributesForItem(at indexPath: IndexPath) function properly. The super class UICollectionViewFlowLayout already puts the correct number of cells in each line so we only have to shift them left within their respective row. The difficulty lies in computing the amount of space we need to shift each cell to the left.
As we want to have a fixed spacing between the cells the core idea is to just assume that the previous cell (the cell left of the cell that is currently laid out) is already positioned properly. Then we only have to add the cell spacing to the maxX value of the previous cell's frame and that's the origin.x value for the current cell's frame.
Now we only need to know when we've reached the beginning of a line, so that we don't align a cell next to a cell in the previous line. (This would not only result in an incorrect layout but it would also be extremely laggy.) So we need to have a recursion anchor. The approach I use for finding that recursion anchor is the following:
To find out if the cell at index i is in the same line as the cell with index i-1 ...
+---------+----------------------------------------------------------------+---------+
| | | |
| | +------------+ | |
| | | | | |
| section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
| inset | |intersection| | | line rect | inset |
| |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| |
| (left) | | | current item | (right) |
| | +------------+ | |
| | previous item | |
+---------+----------------------------------------------------------------+---------+
... I "draw" a rectangle around the current cell and stretch it over the width of the whole collection view. As the UICollectionViewFlowLayout centers all cells vertically every cell in the same line must intersect with this rectangle.
Thus, I simply check if the cell with index i-1 intersects with this line rectangle created from the cell with index i.
If it does intersect, the cell with index i is not the left most cell in the line.
→ Get the previous cell's frame (with the index i−1) and move the current cell next to it.
If it does not intersect, the cell with index i is the left most cell in the line.
→ Move the cell to the left edge of the collection view (without changing its vertical position).
I won't post the actual implementation of the layoutAttributesForItem(at indexPath: IndexPath) function here because I think the most important part is to understand the idea and you can always check my implementation in the source code. (It's a little more complicated than explained here because I also allow .right alignment and various vertical alignment options. However, it follows the same idea.)
Wow, I guess this is the longest answer I've ever written on Stackoverflow. I hope this helps. 😉

The simple solution in 2019
This is one of those depressing questions where things have changed a lot over the years. It is now easy.
Basically you just do this:
// as you move across one row ...
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
// and, obviously start fresh again each row
All you need now is the boilerplate code:
override func layoutAttributesForElements(
in rect: CGRect)->[UICollectionViewLayoutAttributes]? {
guard let att = super.layoutAttributesForElements(in: rect) else { return [] }
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
for a in att {
if a.representedElementCategory != .cell { continue }
if a.frame.origin.y >= y { x = sectionInset.left }
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
y = a.frame.maxY
}
return att
}
Simply copy and paste that in to a UICollectionViewFlowLayout - you're done.
Full working solution to copy and paste:
This is the whole thing:
class TagsLayout: UICollectionViewFlowLayout {
required override init() {super.init(); common()}
required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder); common()}
private func common() {
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
minimumLineSpacing = 10
minimumInteritemSpacing = 10
}
override func layoutAttributesForElements(
in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let att = super.layoutAttributesForElements(in:rect) else {return []}
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
for a in att {
if a.representedElementCategory != .cell { continue }
if a.frame.origin.y >= y { x = sectionInset.left }
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
y = a.frame.maxY
}
return att
}
}
And finally...
Give thanks to #AlexShubin above who first clarified this!
An important issue:
I've found that in some cases - this could be a 2022 Catalyst bug or issue - that strangely you can't reliably set minimumLineSpacing / minimumInteritemSpacing in the bringup code.
I now simply put those lines of code here
class AlignLeftLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(
in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
minimumLineSpacing = 10
minimumInteritemSpacing = 10
for more reliability. This may help someone.
Critical tip for anyone doing, simply, a single-column layout.
It's surprisingly difficult to simply do a "one item per line" collection view. There are a number of different ways to approach it. Here is an extremely simple reliable way:
///The formula for a "one item per line" layout.
class SingleColumnLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(
in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
minimumLineSpacing = 10
minimumInteritemSpacing = CGFloat.greatestFiniteMagnitude
guard let att = super.layoutAttributesForElements(in:rect) else {return []}
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
for a in att {
if a.representedElementCategory != .cell { continue }
if a.frame.origin.y >= y { x = sectionInset.left }
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
y = a.frame.maxY
}
return att
}
}
Note the CGFloat.greatestFiniteMagnitude.

With Swift 4.1 and iOS 11, according to your needs, you may choose one of the 2 following complete implementations in order to fix your problem.
#1. Left align autoresizing UICollectionViewCells
The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout's estimatedItemSize and UILabel's preferredMaxLayoutWidth in order to left align autoresizing cells in a UICollectionView:
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]
let columnLayout = FlowLayout(
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(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return array.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
cell.label.text = array[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 UIKit
class FlowLayout: UICollectionViewFlowLayout {
required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()
estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }
// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Set the initial left inset
var leftInset = sectionInset.left
// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}
return layoutAttributes
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .orange
label.preferredMaxLayoutWidth = 120
label.numberOfLines = 0
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Expected result:
#2. Left align UICollectionViewCells with fixed size
The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:) and UICollectionViewFlowLayout's itemSize in order to left align cells with predefined size in a UICollectionView:
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let columnLayout = FlowLayout(
itemSize: CGSize(width: 140, height: 140),
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(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 7
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
return cell
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}
}
FlowLayout.swift
import UIKit
class FlowLayout: UICollectionViewFlowLayout {
required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()
self.itemSize = itemSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }
// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Set the initial left inset
var leftInset = sectionInset.left
// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}
return layoutAttributes
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .cyan
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Expected result:

If your minimum deployment target is iOS 13, I highly suggest you take advantage of the Compositional Layout (doc here, WWDC presentation here).
I did try some of the top answers here initially. Unfortunately, we encountered an issue wherein some cells tend to disappear intermittently. To us, this happens after calling UICollectionView's reloadData function. It's also important to note that our cells have variable width, a.k.a auto-sizing.
Let me show you an example. Let's say we need to display a page with a list of keyword bubbles.
Here's what you might need in order to accomplish that using Compositional Layout.
override func viewDidLoad() {
super.viewDidLoad()
...
collectionView.collectionViewLayout = createLeftAlignedLayout()
}
private func createLeftAlignedLayout() -> UICollectionViewLayout {
let item = NSCollectionLayoutItem( // this is your cell
layoutSize: NSCollectionLayoutSize(
widthDimension: .estimated(40), // variable width
heightDimension: .absolute(48) // fixed height
)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1.0), // 100% width as inset by its Section
heightDimension: .estimated(50) // variable height; allows for multiple rows of items
),
subitems: [item]
)
group.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
group.interItemSpacing = .fixed(10) // horizontal spacing between cells
return UICollectionViewCompositionalLayout(section: .init(group: group))
}
So as you can see, it's very straightforward.

The question has been up a while but there's no answer and it's a good question. The answer is to override one method in the UICollectionViewFlowLayout subclass:
#implementation MYFlowLayoutSubclass
//Note, the layout's minimumInteritemSpacing (default 10.0) should not be less than this.
#define ITEM_SPACING 10.0f
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];
CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
//this loop assumes attributes are in IndexPath order
for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
if (attributes.frame.origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left; //will add outside loop
} else {
CGRect newLeftAlignedFrame = attributes.frame;
newLeftAlignedFrame.origin.x = leftMargin;
attributes.frame = newLeftAlignedFrame;
}
leftMargin += attributes.frame.size.width + ITEM_SPACING;
[newAttributesForElementsInRect addObject:attributes];
}
return newAttributesForElementsInRect;
}
#end
As recommended by Apple, you get the layout attributes from super and iterate over them. If it's the first in the row (defined by its origin.x being on the left margin), you leave it alone and reset the x to zero. Then for the first cell and every cell, you add the width of that cell plus some margin. This gets passed to the next item in the loop. If it's not the first item, you set it's origin.x to the running calculated margin, and add new elements to the array.

I had the same problem,
Give the Cocoapod UICollectionViewLeftAlignedLayout a try. Just include it in your project and initialize it like this:
UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];

If anyone of you facing issue - some of the cells that's on the right of the collection view exceeding the bounds of the collection view.
Then please use this -
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin : CGFloat = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if Int(layoutAttribute.frame.origin.y) >= Int(maxY) {
leftMargin = sectionInset.left
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
Use INT in place of comparing CGFloat values.

Here is the original answer in Swift. It still works great mostly.
class LeftAlignedFlowLayout: UICollectionViewFlowLayout {
private override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElementsInRect(rect)
var leftMargin = sectionInset.left
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.x == sectionInset.left {
leftMargin = sectionInset.left
}
else {
layoutAttribute.frame.origin.x = leftMargin
}
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
}
return attributes
}
}
Exception: Autosizing Cells
There is one big exception sadly. If you're using UICollectionViewFlowLayout's estimatedItemSize. Internally UICollectionViewFlowLayout is changing things up a bit. I haven't tracked it entirely down but its clear its calling other methods after layoutAttributesForElementsInRect while self sizing cells. From my trial and error I found it seems to call layoutAttributesForItemAtIndexPath for each cell individually during autosizing more often. This updated LeftAlignedFlowLayout works great with estimatedItemSize. It works with static sized cells as well, however the extra layout calls leads me to use the original answer anytime I don't need autosizing cells.
class LeftAlignedFlowLayout: UICollectionViewFlowLayout {
private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttribute = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
// First in a row.
if layoutAttribute?.frame.origin.x == sectionInset.left {
return layoutAttribute
}
// We need to align it to the previous item.
let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
guard let previousLayoutAttribute = self.layoutAttributesForItemAtIndexPath(previousIndexPath) else {
return layoutAttribute
}
layoutAttribute?.frame.origin.x = previousLayoutAttribute.frame.maxX + self.minimumInteritemSpacing
return layoutAttribute
}
}

Building on Michael Sand's answer, I created a subclassed UICollectionViewFlowLayout library to do left, right, or full (basically the default) horizontal justification—it also lets you set the absolute distance between each cell. I plan on adding horizontal center justification and vertical justification to it, too.
https://github.com/eroth/ERJustifiedFlowLayout

Most of the solutions on this page are way too complicated. The easiest way to left justify them, even if there is only 1 cell, is to return the following edge insets:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if collectionView.numberOfItems(inSection: section) == 1 {
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: collectionView.frame.width - flowLayout.itemSize.width)
} else {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
}

In swift. According to Michaels answer
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let oldAttributes = super.layoutAttributesForElementsInRect(rect) else {
return super.layoutAttributesForElementsInRect(rect)
}
let spacing = CGFloat(50) // REPLACE WITH WHAT SPACING YOU NEED
var newAttributes = [UICollectionViewLayoutAttributes]()
var leftMargin = self.sectionInset.left
for attributes in oldAttributes {
if (attributes.frame.origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left
} else {
var newLeftAlignedFrame = attributes.frame
newLeftAlignedFrame.origin.x = leftMargin
attributes.frame = newLeftAlignedFrame
}
leftMargin += attributes.frame.width + spacing
newAttributes.append(attributes)
}
return newAttributes
}

Based on all answers.
Works for leftToRight and rightToLeft
class AlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
let attributes = super.layoutAttributesForElements(in: rect)
let ltr = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight
var leftMargin = ltr ? sectionInset.left : (rect.maxX - sectionInset.right)
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.y >= maxY
{
leftMargin = ltr ? sectionInset.left : (rect.maxX - sectionInset.right)
}
layoutAttribute.frame.origin.x = leftMargin - (ltr ? 0 : layoutAttribute.frame.width)
if (ltr)
{
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
}
else
{
leftMargin -= layoutAttribute.frame.width + minimumInteritemSpacing
}
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}

Based on all answers, I change a bit and it works good for me
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.y >= maxY
|| layoutAttribute.frame.origin.x == sectionInset.left {
leftMargin = sectionInset.left
}
if layoutAttribute.frame.origin.x == sectionInset.left {
leftMargin = sectionInset.left
}
else {
layoutAttribute.frame.origin.x = leftMargin
}
leftMargin += layoutAttribute.frame.width
maxY = max(layoutAttribute.frame.maxY, maxY)
}
return attributes
}

Based on answers here, but fixed crashes and aligning problems when your collection view is also got headers or footers. Aligning left only cells:
class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var prevMaxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.y >= prevMaxY {
leftMargin = sectionInset.left
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
prevMaxY = layoutAttribute.frame.maxY
}
return attributes
}
}

Thanks for the Michael Sand's answer. I modified it to a solution of multiple rows (the same alignment Top y of each row) that is Left Alignment, even spacing to each item.
static CGFloat const ITEM_SPACING = 10.0f;
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
CGRect contentRect = {CGPointZero, self.collectionViewContentSize};
NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:contentRect];
NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];
CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
NSMutableDictionary *leftMarginDictionary = [[NSMutableDictionary alloc] init];
for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
UICollectionViewLayoutAttributes *attr = attributes.copy;
CGFloat lastLeftMargin = [[leftMarginDictionary valueForKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]] floatValue];
if (lastLeftMargin == 0) lastLeftMargin = leftMargin;
CGRect newLeftAlignedFrame = attr.frame;
newLeftAlignedFrame.origin.x = lastLeftMargin;
attr.frame = newLeftAlignedFrame;
lastLeftMargin += attr.frame.size.width + ITEM_SPACING;
[leftMarginDictionary setObject:#(lastLeftMargin) forKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]];
[newAttributesForElementsInRect addObject:attr];
}
return newAttributesForElementsInRect;
}

Here is my journey to find the best code that works with Swift 5. I have joined couple of answers from this thread and some other threads to solve warnings and issues that I faced. I had a warning and some abnormal behavior when scrolling through my collection view. The console prints the following:
This is likely occurring because the flow layout "xyz" is modifying attributes returned by UICollectionViewFlowLayout without copying them.
Another issue I faced is that some lengthy cells are getting cropped at the right side of the screen. Also, I was setting the section insets and the minimumInteritemSpacing in the delegate functions which resulted in values were not reflected in the custom class. The fix for that was setting those attributes to an instance of the layout before applying it to my collection view.
Here's how I used the layout for my collection view:
let layout = LeftAlignedCollectionViewFlowLayout()
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 7.5
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
super.init(frame: frame, collectionViewLayout: layout)
Here's the flow layout class
class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if Int(layoutAttribute.frame.origin.y) >= Int(maxY) || layoutAttribute.frame.origin.x == sectionInset.left {
leftMargin = sectionInset.left
}
if layoutAttribute.frame.origin.x == sectionInset.left {
leftMargin = sectionInset.left
}
else {
layoutAttribute.frame.origin.x = leftMargin
}
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}

The problem with UICollectionView is that it tries to automatically fit the cells in the available area.
I have done this by first defining number of rows and columns and then defining the cell size for that row and column
1) To define Sections (Rows) of my UICollectionView:
(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
2) To define number of items in a section. You can define different number of items for every section. you can get section number using 'section' parameter.
(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
3) To define Cell size for each section and row separately. You can get section number and row number using the 'indexPath' parameter i.e. [indexPath section] for section number and [indexPath row] for row number.
(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
4) Then you can display your cells in rows and sections using:
(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
NOTE:
In UICollectionView
Section == Row
IndexPath.Row == Column

Mike Sand's answer is good but i had experienced some issues with this code (Like lengthy cell clipped out). And new code:
#define ITEM_SPACE 7.0f
#implementation LeftAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
if (nil == attributes.representedElementKind) {
NSIndexPath* indexPath = attributes.indexPath;
attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
}
}
return attributesToReturn;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* currentItemAttributes =
[super layoutAttributesForItemAtIndexPath:indexPath];
UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];
if (indexPath.item == 0) { // first item of section
CGRect frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width + ITEM_SPACE;
CGRect currentFrame = currentItemAttributes.frame;
CGRect strecthedCurrentFrame = CGRectMake(0,
currentFrame.origin.y,
self.collectionView.frame.size.width,
currentFrame.size.height);
if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
// the approach here is to take the current frame, left align it to the edge of the view
// then stretch it the width of the collection view, if it intersects with the previous frame then that means it
// is on the same line, otherwise it is on it's own new line
CGRect frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
CGRect frame = currentItemAttributes.frame;
frame.origin.x = previousFrameRightPoint;
currentItemAttributes.frame = frame;
return currentItemAttributes;
}

The above code works for me. I would like to share the respective Swift 3.0 code.
class SFFlowLayout: UICollectionViewFlowLayout {
let itemSpacing: CGFloat = 3.0
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attriuteElementsInRect = super.layoutAttributesForElements(in: rect)
var newAttributeForElement: Array<UICollectionViewLayoutAttributes> = []
var leftMargin = self.sectionInset.left
for tempAttribute in attriuteElementsInRect! {
let attribute = tempAttribute
if attribute.frame.origin.x == self.sectionInset.left {
leftMargin = self.sectionInset.left
}
else {
var newLeftAlignedFrame = attribute.frame
newLeftAlignedFrame.origin.x = leftMargin
attribute.frame = newLeftAlignedFrame
}
leftMargin += attribute.frame.size.width + itemSpacing
newAttributeForElement.append(attribute)
}
return newAttributeForElement
}
}

Edited Angel García Olloqui's answer to respect minimumInteritemSpacing from delegate's collectionView(_:layout:minimumInteritemSpacingForSectionAt:), if it implements it.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.y >= maxY {
leftMargin = sectionInset.left
}
layoutAttribute.frame.origin.x = leftMargin
let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout
let spacing = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: 0) ?? minimumInteritemSpacing
leftMargin += layoutAttribute.frame.width + spacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}

I used more complex design in my app where a tableView cell may have a collectionView so the best solution i got was not hard code but I used this library instead:
https://github.com/rubygarage/collection-view-layouts
and here's my implementation:
import collection_view_layouts
class CIFilterBubblesCell: UITableViewCell, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, LayoutDelegate{
#IBOutlet weak var filterBubblesCollectionView: UICollectionView!
var bubbleFilters : [BubbleViewData] = []
let cellID = "BubbleCellID"
let cellName = "BubbleCell"
private var cellSizes = [[CGSize]]()
override func awakeFromNib() {
super.awakeFromNib()
}
func setupCell(data: [BubbleViewData])
{
bubbleFilters = data
prepareCellSizes()
filterBubblesCollectionView.register(UINib(nibName: cellName, bundle: nil), forCellWithReuseIdentifier: cellID)
filterBubblesCollectionView.delegate = self
filterBubblesCollectionView.dataSource = self
let layout: TagsLayout = TagsLayout()
layout.delegate = self
layout.cellsPadding = ItemsPadding(horizontal: 5, vertical: 5)
layout.contentAlign = .left
filterBubblesCollectionView.collectionViewLayout = layout
filterBubblesCollectionView.reloadData()
self.layoutIfNeeded()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
bubbleFilters.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//...
}
func cellSize(indexPath: IndexPath) -> CGSize {
return cellSizes[indexPath.section][indexPath.row]
}
private func prepareCellSizes() {
cellSizes.removeAll()
var sizes: [CGSize] = []
bubbleFilters.forEach {item in
var size = item.name.sizeOfString(usingFont: UIFont.systemFont(ofSize: 17))
size.width += 30
size.height += 10
if (UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad)
{
size.width += 70
size.height += 10
}
sizes.append(size)
}
cellSizes.append(sizes)
}
}
Don't forget to add this extension:
extension String {
func sizeOfString(usingFont font: UIFont) -> CGSize {
let fontAttributes = [NSAttributedString.Key.font: font]
return self.size(withAttributes: fontAttributes)
}
}

As of Jan 2021,
Angel's answer is still relevant. You just need to create a custom flow layout (and set your collectionview to use that custom flow layout) but the only thing you need to add to that custom class is this method (answer in objective C):
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
CGFloat maxY = -1.0f;
//this loop assumes attributes are in IndexPath order
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if (attribute.frame.origin.y >= maxY) {
leftMargin = self.sectionInset.left;
}
attribute.frame = CGRectMake(leftMargin, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);
leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing;
maxY = MAX(CGRectGetMaxY(attribute.frame), maxY);
}
return attributes;
}

If you not serious custom view anything you can set Scroll Direction in Horizontal replace Vertical then try build run the item cell will begin in left not center

Related

iOS: Collection view custom tag layout dynamic Height Swift

I am trying to create tag list view. Unfortunately I don't know How to work with Custom flow layout. But I found to create pretty tag list. But encountered a problem with multi-line label. If we used the text more than the collection view width, then the attribute.frame.height should be multiplied like 40*numberOfLines. Please help to solve this problem.
Custom Flow Layout:
import UIKit
class CollectionViewRow {
var attributes = [UICollectionViewLayoutAttributes]()
var spacing: CGFloat = 0
init(spacing: CGFloat) {
self.spacing = spacing
}
func add(attribute: UICollectionViewLayoutAttributes) {
attributes.append(attribute)
}
var rowWidth: CGFloat {
return attributes.reduce(0, { result, attribute -> CGFloat in
return result + attribute.frame.width
}) + CGFloat(attributes.count - 1) * spacing
}
func layout(collectionViewWidth: CGFloat) {
var offset = spacing
for attribute in attributes {
attribute.frame.origin.x = offset
offset += attribute.frame.width + spacing
}
}
}
class UICollectionViewCenterLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var rows = [CollectionViewRow]()
var currentRowY: CGFloat = -1
for attribute in attributes {
if currentRowY != attribute.frame.midY {
currentRowY = attribute.frame.midY
rows.append(CollectionViewRow(spacing: 10))
}
rows.last?.add(attribute: attribute)
}
rows.forEach { $0.layout(collectionViewWidth: collectionView?.frame.width ?? 0) }
return rows.flatMap { $0.attributes }
}
}
Usage:
let layout = UICollectionViewCenterLayout()
layout.estimatedItemSize = CGSize(width: 140, height: 40)
collectionView.collectionViewLayout = layout
collectionView.reloadData()
I tried to change height but It not working expected:
func layout(collectionViewWidth: CGFloat) {
var offset = spacing
for attribute in attributes {
attribute.frame.origin.x = offset
let attributeTotalWidth = attribute.frame.width + spacing
if attributeTotalWidth > collectionViewWidth{
let multiplier: CGFloat = attributeTotalWidth/collectionViewWidth
let intVal = CGFloat(Int(multiplier))
let fullNumber = multiplier-intVal > 0 ? intVal+1 :
attribute.frame.size.height = fullNumber * 40
}
offset += attributeTotalWidth
}
}
Can you please help me to find out the solution to make variable item height based on the label text content? Thanking you in advance!
#iDeveloper I tried to fix your code.
Here are the things you can do.
Simplify your array.
let titles = ["Apple","Google","Computer", "Terminal", "Gross", "Form lands", "Water lands", "river", "mounts", "trees", "places", "parks", "towns", "cities","Lorem Ipsum is simply dummy text of the printing and typesettingindustry.","Hello","A","B","CAD","John","Nick","Lucas","Amy","Lawrance","Apple","Google","Computer", "Browser","Overflow","Hi","Hello","A","B","CAD","John","Nick","Lucas","Amy","Lawrance", "Lorem Ipsum is simply dummy text of the printing and typesetting industry.",""]
You have to add a width constraint to your title label like this.
Set content hugging priority for you label
End result
One could make the below code prettier by moving more code into Row, but it fixes the issue of clipped labels (as seen in the result image of the other answer). The code is also a lot simpler and still works with labels that contain multiple lines (label.numberOfLines = 0)
class CustomCollectionViewLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
let collectionViewWidth = collectionView?.frame.width ?? 0
var rows = [Row]()
let spacing: CGFloat = 10
var offsetY: CGFloat = 0
var offsetX: CGFloat = spacing
rows.append(Row())
for attribute in attributes {
let neededWidth = CGFloat(attribute.frame.width) + spacing
let neededMaxX = offsetX + neededWidth
if neededMaxX <= collectionViewWidth {
attribute.frame.origin.x = offsetX
attribute.frame.origin.y = offsetY
offsetX = neededMaxX
rows.last?.add(attribute: attribute)
} else {
attribute.frame.origin.x = spacing
offsetX = attribute.frame.origin.x + neededWidth
offsetY += attribute.frame.height + spacing
attribute.frame.origin.y = offsetY
rows.append(Row())
rows.last?.add(attribute: attribute)
}
}
return rows.flatMap { $0.attributes }
}
}
class Row {
var attributes = [UICollectionViewLayoutAttributes]()
init() {}
func add(attribute: UICollectionViewLayoutAttributes) {
attributes.append(attribute)
}
}

Autoresize UICollectionView cells, Align cell to top

I've a UICollectionView, with multiple sections and rows.
Header and Footer views wherever necessary, of fixed size.
Cells that are autoresizable
The cell view are designed like :
Green colour - ImageView
Orange colour - Labels with numberOfLines = 0.
The cell should expand it's size according to label numberOfLines.
I've achieved this using this code in MyCustomCell :
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
super.apply(layoutAttributes)
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
let targetSize = CGSize(width: Constants.screenWidth/3.5, height: 0)
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
The cells are autoresizing but the contentView (in cyan colour) are Centre aligned both vertically and horizontally.
I need to make it vertically align to Top.
I had the alignment problem with headers and footers too. For that i've subclassed UICollectionViewFlowLayout
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext {
let context: UICollectionViewLayoutInvalidationContext = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes)
let indexPath = preferredAttributes.indexPath
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter, at: [indexPath])
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader, at: [indexPath])
return context
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var topMargin = sectionInset.top
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.y >= maxY {
leftMargin = sectionInset.left
topMargin = sectionInset.top
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
Here is an image to illustrate current situation. The cyan are contentView of cells. I've to make it align to Top.
EDIT:
So i realised that UICollectionViewFlowLayout code was creating more bugs than fixing a problem. I added layoutAttributesForElements to left align my cell in case there is only one cell. What it actually did was align all of my cells to the left.
Modified the code as
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
if attributes?.count == 1 {
if let currentAttribute = attributes?.first {
currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
}
}
return attributes
}
}
Now my UICollectionViewCell are properly aligned to horizontal centre with exception to if only one cell which will be left aligned.
Still no solution for vertical alignment.
So I worked around this for quite a bit. No answers from stack overflow help in any way. So i played with custom UICollectionViewFlowLayout method layoutAttributesForElements.
This method will call for every section, and will have layoutAttributesForElements for rect. This will give an array of UICollectionViewLayoutAttributes which one can modify as per the need.
I was having problem figuring out the y coordinate which I would like to change for my cells.
So first with a loop in attributes from layoutAttributesForElements I got the header and saved it's height in a variable. Then changed the rest of cell's y coordinate with the headerHeight value.
Not sure if this is the right way or not, nor did any performance testing. For now everything seems fine without any lag or jerk.
Here is full code or custom UICollectionViewFlowLayout.
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attr = super.layoutAttributesForElements(in: rect)
var attributes = [UICollectionViewLayoutAttributes]()
for itemAttributes in attr! {
let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
// manipulate itemAttributesCopy
attributes.append(itemAttributesCopy)
}
if attributes.count == 1 {
if let currentAttribute = attributes.first {
currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
}
} else {
var sectionHeight: CGFloat = 0
attributes.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .supplementaryView else {
return
}
if layoutAttribute.representedElementKind == UICollectionView.elementKindSectionHeader {
sectionHeight = layoutAttribute.frame.size.height
}
}
attributes.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.x == 0 {
sectionHeight = max(layoutAttribute.frame.minY, sectionHeight)
}
layoutAttribute.frame = CGRect(origin: CGPoint(x: layoutAttribute.frame.origin.x, y: sectionHeight), size: layoutAttribute.frame.size)
}
}
return attributes
}
}
NOTE: You need to copy the attributes in an array first to work with. Else xcode will yell Logging only once for UICollectionViewFlowLayout cache mismatched frame in console.
If any new/perfect solution is there please let me know.
CHEERS!!!

How to set UICollectionView bottom padding and scrolling size

Hello I have working uicollectionview custom layout and i have issuesfor bottom padding and scrolling sizes. Under below picture you can see;
Also top side first cell always stays on top when scrolling ( Left side cell must be always stay right now no problem there , but top side cell must be scroll when scrolling )
I try to set in viewDidLoad under below codes but dont work
self.automaticallyAdjustsScrollViewInsets = false
self.collectionView.contentInset = UIEdgeInsetsMake(20, 20, 120, 100);
My custom collectionview layout file
import UIKit
public var CELL_HEIGHT = CGFloat(22)
protocol CustomCollectionViewDelegateLayout: NSObjectProtocol {
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, widthForItemAtIndexPath indexPath: NSIndexPath) -> CGFloat
}
class CustomCollectionViewLayout: UICollectionViewLayout {
// Used for calculating each cells CGRect on screen.
// CGRect will define the Origin and Size of the cell.
let STATUS_BAR = UIApplication.sharedApplication().statusBarFrame.height
// Dictionary to hold the UICollectionViewLayoutAttributes for
// each cell. The layout attribtues will define the cell's size
// and position (x, y, and z index). I have found this process
// to be one of the heavier parts of the layout. I recommend
// holding onto this data after it has been calculated in either
// a dictionary or data store of some kind for a smooth performance.
var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
// Defines the size of the area the user can move around in
// within the collection view.
var contentSize = CGSize.zero
// Used to determine if a data source update has occured.
// Note: The data source would be responsible for updating
// this value if an update was performed.
var dataSourceDidUpdate = true
weak var delegate: CustomCollectionViewDelegateLayout?
override func collectionViewContentSize() -> CGSize {
return self.contentSize
}
override func prepareLayout() {
// Only update header cells.
if !dataSourceDidUpdate {
// Determine current content offsets.
let xOffset = collectionView!.contentOffset.x
let yOffset = collectionView!.contentOffset.y
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Confirm the section has items.
if collectionView?.numberOfItemsInSection(section) > 0 {
// Update all items in the first row.
if section == 0 {
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
// Build indexPath to get attributes from dictionary.
let indexPath = NSIndexPath(forItem: item, inSection: section)
// Update y-position to follow user.
if let attrs = cellAttrsDictionary[indexPath] {
var frame = attrs.frame
// Also update x-position for corner cell.
if item == 0 {
frame.origin.x = xOffset
}
frame.origin.y = yOffset
attrs.frame = frame
}
}
// For all other sections, we only need to update
// the x-position for the fist item.
} else {
// Build indexPath to get attributes from dictionary.
let indexPath = NSIndexPath(forItem: 0, inSection: section)
// Update y-position to follow user.
if let attrs = cellAttrsDictionary[indexPath] {
var frame = attrs.frame
frame.origin.x = xOffset
attrs.frame = frame
}
}
}
}
}
// Do not run attribute generation code
// unless data source has been updated.
return
}
// Acknowledge data source change, and disable for next time.
dataSourceDidUpdate = false
var maxWidthInASection = CGFloat(0)
// Cycle through each section of the data source.
if collectionView?.numberOfSections() > 0 {
for section in 0...collectionView!.numberOfSections()-1 {
// Cycle through each item in the section.
if collectionView?.numberOfItemsInSection(section) > 0 {
var prevCellAttributes: UICollectionViewLayoutAttributes?
for item in 0...collectionView!.numberOfItemsInSection(section)-1 {
let cellIndex = NSIndexPath(forItem: item, inSection: section)
guard let width = delegate?.collectionView(collectionView!, layout: self, widthForItemAtIndexPath: cellIndex) else {
print("Please comform to CustomCollectionViewDelegateLayout protocol")
return
}
// Build the UICollectionVieLayoutAttributes for the cell.
var xPos = CGFloat(0)
let yPos = CGFloat(section) * CELL_HEIGHT
if let prevCellAttributes = prevCellAttributes {
xPos = CGRectGetMaxX(prevCellAttributes.frame)
}
let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: cellIndex)
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: width, height: CELL_HEIGHT)
// Determine zIndex based on cell type.
if section == 0 && item == 0 {
cellAttributes.zIndex = 4
} else if section == 0 {
cellAttributes.zIndex = 3
} else if item == 0 {
cellAttributes.zIndex = 2
} else {
cellAttributes.zIndex = 1
}
// Save the attributes.
cellAttrsDictionary[cellIndex] = cellAttributes
prevCellAttributes = cellAttributes
let maxX = CGRectGetMaxX(cellAttributes.frame)
if maxWidthInASection < maxX {
maxWidthInASection = maxX
}
}
}
}
}
// Update content size.
let contentWidth = maxWidthInASection
let contentHeight = Double(collectionView!.numberOfSections())
self.contentSize = CGSize(width: Double(contentWidth), height: contentHeight)
self.contentSize.height = CGFloat(Double(collectionView!.numberOfSections()))
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Create an array to hold all elements found in our current view.
var attributesInRect = [UICollectionViewLayoutAttributes]()
// Check each element to see if it should be returned.
for cellAttributes in cellAttrsDictionary.values.elements {
if CGRectIntersectsRect(rect, cellAttributes.frame) {
attributesInRect.append(cellAttributes)
}
}
// Return list of elements.
return attributesInRect
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttrsDictionary[indexPath]!
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
}
I try change collecionview properties in storyboard but doesnt works also i attached current collectionview object properties picture under below
Also i selected zoom 4 but zoom feature doesnt work ? why ? it must be work also zoom feature.
I know this is too late to answer, but this could be useful to other users.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 0)
}
This what worked for me:
messagesCollectionView.contentInset.top = 100
messagesCollectionView.contentInset.bottom = 80
This can be also done on IB under section insets...

How to Set Collectionview cell margin in all layout [duplicate]

How do I set cell spacing in a section of UICollectionView? I know there is a property minimumInteritemSpacing I have set it to 5.0 still the spacing is not appearing 5.0. I have implemented the flowout delegate method.
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return 5.0;
}
still I am not getting the desired result. I think its the minimum spacing . Isn't there any way by which I can set the maximum spacing?
Supporting the initial question. I tried to get the spacing to 5px on the UICollectionView but this does not work, as well with a UIEdgeInsetsMake(0,0,0,0)...
On a UITableView I can do this by directly specifying the x,y coordinates in a row...
Heres my UICollectionView code:
#pragma mark collection view cell layout / size
- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return [self getCellSize:indexPath]; // will be w120xh100 or w190x100
// if the width is higher, only one image will be shown in a line
}
#pragma mark collection view cell paddings
- (UIEdgeInsets)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0, 0, 0, 0); // top, left, bottom, right
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return 5.0;
}
Update: Solved my problem, with the following code.
ViewController.m
#import "ViewController.h"
#import "MagazineCell.h" // created just the default class.
static NSString * const cellID = #"cellID";
#interface ViewController ()
#end
#implementation ViewController
#pragma mark - Collection view
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 30;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MagazineCell *mCell = (MagazineCell *)[collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];
mCell.backgroundColor = [UIColor lightGrayColor];
return mCell;
}
#pragma mark Collection view layout things
// Layout: Set cell size
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"SETTING SIZE FOR ITEM AT INDEX %d", indexPath.row);
CGSize mElementSize = CGSizeMake(104, 104);
return mElementSize;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return 2.0;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 2.0;
}
// Layout: Set Edges
- (UIEdgeInsets)collectionView:
(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
// return UIEdgeInsetsMake(0,8,0,8); // top, left, bottom, right
return UIEdgeInsetsMake(0,0,0,0); // top, left, bottom, right
}
#end
I know that the topic is old, but in case anyone still needs correct answer here what you need:
Override standard flow layout.
Add implementation like that:
- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *answer = [super layoutAttributesForElementsInRect:rect];
for(int i = 1; i < [answer count]; ++i) {
UICollectionViewLayoutAttributes *currentLayoutAttributes = answer[i];
UICollectionViewLayoutAttributes *prevLayoutAttributes = answer[i - 1];
NSInteger maximumSpacing = 4;
NSInteger origin = CGRectGetMaxX(prevLayoutAttributes.frame);
if(origin + maximumSpacing + currentLayoutAttributes.frame.size.width < self.collectionViewContentSize.width) {
CGRect frame = currentLayoutAttributes.frame;
frame.origin.x = origin + maximumSpacing;
currentLayoutAttributes.frame = frame;
}
}
return answer;
}
where maximumSpacing could be set to any value you prefer. This trick guarantees that the space between cells would be EXACTLY equal to maximumSpacing!!
Using a horizontal flow layout, I was also getting a 10 points spacing between cells. To remove the spacing I needed to set minimumLineSpacing as well as minimumInterItemSpacing to zero.
UICollectionViewFlowLayout *flow = [[UICollectionViewFlowLayout alloc] init];
flow.itemSize = CGSizeMake(cellWidth, cellHeight);
flow.scrollDirection = UICollectionViewScrollDirectionHorizontal;
flow.minimumInteritemSpacing = 0;
flow.minimumLineSpacing = 0;
Also, if all your cells are the same size, it's simpler and more efficient to set the property on the flow layout directly instead of using delegate methods.
Remember, it is minimum line space, not minimum inter item spacing or cell space. Because your collectionView's scroll direction is HORIZONTAL.
If it is vertical then you need to set cell space or inter item space for horizontal space between cells and line spacing for vertical space between cells.
Objective-C version
- (CGFloat)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 20;
}
Swift version:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
Try this code to ensure you have a spacing of 5px between each item:
- (UIEdgeInsets)collectionView:(UICollectionView *) collectionView
layout:(UICollectionViewLayout *) collectionViewLayout
insetForSectionAtIndex:(NSInteger) section {
return UIEdgeInsetsMake(0, 0, 0, 5); // top, left, bottom, right
}
- (CGFloat)collectionView:(UICollectionView *) collectionView
layout:(UICollectionViewLayout *) collectionViewLayout
minimumInteritemSpacingForSectionAtIndex:(NSInteger) section {
return 5.0;
}
Swift version of the most popular answer. Space between the cells will be equal to cellSpacing.
class CustomViewFlowLayout : UICollectionViewFlowLayout {
let cellSpacing:CGFloat = 4
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if let attributes = super.layoutAttributesForElementsInRect(rect) {
for (index, attribute) in attributes.enumerate() {
if index == 0 { continue }
let prevLayoutAttributes = attributes[index - 1]
let origin = CGRectGetMaxX(prevLayoutAttributes.frame)
if(origin + cellSpacing + attribute.frame.size.width < self.collectionViewContentSize().width) {
attribute.frame.origin.x = origin + cellSpacing
}
}
return attributes
}
return nil
}
}
I have found very easy way to configure spacing between cells or rows by using IB.
Just select UICollectionView from storyboard/Xib file and click in Size Inspector as specified in below image.
For configuring space programatically use following properties.
1) For setting space between rows.
[self.collectionView setMinimumLineSpacing:5];
2) For setting space between items/cells.
[self.collectionView setMinimumInteritemSpacing:5];
Please note the property name minimumInterItemSpacing . This will be the minimum spacing between the items not the exact spacing. If you set minimumInterItemSpacing to some value you can assure that spacing wont be a value less than that. But there is a chance get a higher value.
Actually the spacing between items depends on several factors itemSize and sectionInset. Collection view dynamically place the contents based on these values. So you cannot assure the exact spacing. You should do some trial and error with sectionInset and minimumInterItemSpacing.
Answer for Swift 3.0, Xcode 8
1.Make sure you set collection view delegate
class DashboardViewController: UIViewController {
#IBOutlet weak var dashboardCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
dashboardCollectionView.delegate = self
}
}
2.Implement UICollectionViewDelegateFlowLayout protocol, not UICollectionViewDelegate.
extension DashboardViewController: UICollectionViewDelegateFlowLayout {
fileprivate var sectionInsets: UIEdgeInsets {
return .zero
}
fileprivate var itemsPerRow: CGFloat {
return 2
}
fileprivate var interitemSpace: CGFloat {
return 5.0
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let sectionPadding = sectionInsets.left * (itemsPerRow + 1)
let interitemPadding = max(0.0, itemsPerRow - 1) * interitemSpace
let availableWidth = collectionView.bounds.width - sectionPadding - interitemPadding
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return sectionInsets
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return interitemSpace
}
}
Simple code for spacing
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.minimumInteritemSpacing = 10 // Some float value
The voted answer (and also the swift version) has a small issue: there will be a big spacing on the right.
This is because the flow layout is customised to make the cell spacing exact, with a float left behaviour.
My solution is to manipulate the section inset, so that the section is align center, yet the spacing is exactly as specified.
In screenshot below, the item/line spacing is exactly 8pt, while the section left & right inset will be bigger than 8pt (to make it center aligned):
Swift code as such:
private let minItemSpacing: CGFloat = 8
private let itemWidth: CGFloat = 100
private let headerHeight: CGFloat = 32
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Create our custom flow layout that evenly space out the items, and have them in the center
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
layout.minimumInteritemSpacing = minItemSpacing
layout.minimumLineSpacing = minItemSpacing
layout.headerReferenceSize = CGSize(width: 0, height: headerHeight)
// Find n, where n is the number of item that can fit into the collection view
var n: CGFloat = 1
let containerWidth = collectionView.bounds.width
while true {
let nextN = n + 1
let totalWidth = (nextN*itemWidth) + (nextN-1)*minItemSpacing
if totalWidth > containerWidth {
break
} else {
n = nextN
}
}
// Calculate the section inset for left and right.
// Setting this section inset will manipulate the items such that they will all be aligned horizontally center.
let inset = max(minItemSpacing, floor( (containerWidth - (n*itemWidth) - (n-1)*minItemSpacing) / 2 ) )
layout.sectionInset = UIEdgeInsets(top: minItemSpacing, left: inset, bottom: minItemSpacing, right: inset)
collectionView.collectionViewLayout = layout
}
Storyboard Approach
Select CollectionView in your storyboard and go to size inspector and set min spacing for cells and lines as 5
Swift 5 Programmatically
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
//Provide Width and Height According to your need
let width = UIScreen.main.bounds.width / 4
let height = UIScreen.main.bounds.height / 10
layout.itemSize = CGSize(width: width, height: height)
//For Adjusting the cells spacing
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 5
return UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
}()
Define UICollectionViewDelegateFlowLayout protocol in your header file.
Implement following method of UICollectionViewDelegateFlowLayout protocol like this:
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(5, 5, 5, 5);
}
Click Here to see Apple Documentation of UIEdgeInsetMake method.
If u want to tweak the spacing without touching the actual cell size, this is the solution that worked best for me. #xcode 9 #tvOS11 #iOS11 #swift
So in UICollectionViewDelegateFlowLayout, change implement the next methods, the trick is u have to use both of them, and the documentation was not really pointing me to think in that direction. :D
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return cellSpacing
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return cellSpacing
}
Well, if you're creating a horizontal collection view then to give space between the cells, you need to set the property minimumLineSpacing .
I'm using monotouch, so the names and code will be a bit different, but you can do this by making sure that the width of the collectionview equals (x * cell width) + (x-1) * MinimumSpacing with x = amount of cells per row.
Just do following steps based on your MinimumInteritemSpacing and the Width of the Cell
1) We calculate amount of items per row based on cell size + current insets + minimum spacing
float currentTotalWidth = CollectionView.Frame.Width - Layout.SectionInset.Left - Layout.SectionInset.Right (Layout = flowlayout)
int amountOfCellsPerRow = (currentTotalWidth + MinimumSpacing) / (cell width + MinimumSpacing)
2) Now you have all info to calculate the expected width for the collection view
float totalWidth =(amountOfCellsPerRow * cell width) + (amountOfCellsPerRow-1) * MinimumSpacing
3) So the difference between the current width and the expected width is
float difference = currentTotalWidth - totalWidth;
4) Now adjust the insets (in this example we add it to the right, so the left position of the collectionview stays the same
Layout.SectionInset.Right = Layout.SectionInset.Right + difference;
I have a horizontal UICollectionView and subclassed UICollectionViewFlowLayout. The collection view has large cells, and only shows one row of them at a time, and the collection view fits the width of the screen.
I tried iago849's answer and it worked, but then I found out I didn't even need his answer. For some reason, setting the minimumInterItemSpacing does nothing. The spacing between my items/cells can be entirely controlled by minimumLineSpacing.
Not sure why it works this way, but it works.
My solution in Swift 3 cell line spacing like in Instagram:
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.rgb(red: 227, green: 227, blue: 227)
cv.showsVerticalScrollIndicator = false
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 1
layout.minimumInteritemSpacing = 1
return cv
}()
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
switch UIDevice.current.modelName {
case "iPhone 4":
return CGSize(width: 106, height: 106)
case "iPhone 5":
return CGSize(width: 106, height: 106)
case "iPhone 6,7":
return CGSize(width: 124, height: 124)
case "iPhone Plus":
return CGSize(width: 137, height: 137)
default:
return CGSize(width: frame.width / 3, height: frame.width / 3)
}
}
How to detect device programmaticlly:
https://stackoverflow.com/a/26962452/6013170
I stumbled upon a similar problem as OP. Unfortunately the accepted answer did not work for me since the content of the collectionView would not be centered properly. Therefore I came up with a different solution which only requires that all items in the collectionView are of the same width, which seems to be the case in the question:
#define cellSize 90
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
float width = collectionView.frame.size.width;
float spacing = [self collectionView:collectionView layout:collectionViewLayout minimumInteritemSpacingForSectionAtIndex:section];
int numberOfCells = (width + spacing) / (cellSize + spacing);
int inset = (width + spacing - numberOfCells * (cellSize + spacing) ) / 2;
return UIEdgeInsetsMake(0, inset, 0, inset);
}
That code will ensure that the value returned by ...minimumInteritemSpacing... will be the exact spacing between every collectionViewCell and furthermore guarantee that the cells all together will be centered in the collectionView
The above solution by vojtech-vrbka is correct but it triggers a warning:
warning:UICollectionViewFlowLayout has cached frame mismatch for index path - cached value: This is likely occurring because the flow layout subclass Layout is modify attributes returned by UICollectionViewFlowLayout without copying them
The following code should fix it:
class CustomViewFlowLayout : UICollectionViewFlowLayout {
let cellSpacing:CGFloat = 4
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let original = super.layoutAttributesForElementsInRect(rect)
if let original = original {
let attributes = NSArray.init(array: original, copyItems: true) as! [UICollectionViewLayoutAttributes]
for (index, attribute) in attributes.enumerate() {
if index == 0 { continue }
let prevLayoutAttributes = attributes[index - 1]
let origin = CGRectGetMaxX(prevLayoutAttributes.frame)
if(origin + cellSpacing + attribute.frame.size.width < self.collectionViewContentSize().width) {
attribute.frame.origin.x = origin + cellSpacing
}
}
return attributes
}
return nil
}
}
I have problem with the accepted answer, so I updated it, this is working for me:
.h
#interface MaxSpacingCollectionViewFlowLayout : UICollectionViewFlowLayout
#property (nonatomic,assign) CGFloat maxCellSpacing;
#end
.m
#implementation MaxSpacingCollectionViewFlowLayout
- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
if (attributes.count <= 0) return attributes;
CGFloat firstCellOriginX = ((UICollectionViewLayoutAttributes *)attributes[0]).frame.origin.x;
for(int i = 1; i < attributes.count; i++) {
UICollectionViewLayoutAttributes *currentLayoutAttributes = attributes[i];
if (currentLayoutAttributes.frame.origin.x == firstCellOriginX) { // The first cell of a new row
continue;
}
UICollectionViewLayoutAttributes *prevLayoutAttributes = attributes[i - 1];
CGFloat prevOriginMaxX = CGRectGetMaxX(prevLayoutAttributes.frame);
if ((currentLayoutAttributes.frame.origin.x - prevOriginMaxX) > self.maxCellSpacing) {
CGRect frame = currentLayoutAttributes.frame;
frame.origin.x = prevOriginMaxX + self.maxCellSpacing;
currentLayoutAttributes.frame = frame;
}
}
return attributes;
}
#end
Swift 3 Version
Simply create a UICollectionViewFlowLayout subclass and paste this method.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let answer = super.layoutAttributesForElements(in: rect) else { return nil }
for i in 1..<answer.count {
let currentAttributes = answer[i]
let previousAttributes = answer[i - 1]
let maximumSpacing: CGFloat = 8
let origin = previousAttributes.frame.maxX
if (origin + maximumSpacing + currentAttributes.frame.size.width < self.collectionViewContentSize.width && currentAttributes.frame.origin.x > previousAttributes.frame.origin.x) {
var frame = currentAttributes.frame
frame.origin.x = origin + maximumSpacing
currentAttributes.frame = frame
}
}
return answer
}
I have tried iago849's answer and it worked.
Swift 4
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let answer = super.layoutAttributesForElements(in: rect) else {
return nil
}
let count = answer.count
for i in 1..<count {
let currentLayoutAttributes = answer[i]
let prevLayoutAttributes = answer[i-1]
let origin = prevLayoutAttributes.frame.maxX
if (origin + CGFloat(spacing) + currentLayoutAttributes.frame.size.width) < self.collectionViewContentSize.width && currentLayoutAttributes.frame.origin.x > prevLayoutAttributes.frame.origin.x {
var frame = currentLayoutAttributes.frame
frame.origin.x = origin + CGFloat(spacing)
currentLayoutAttributes.frame = frame
}
}
return answer
}
Here is the link for the github project.
https://github.com/vishalwaka/MultiTags
Previous versions did not really work with sections > 1. So my solution was found here https://codentrick.com/create-a-tag-flow-layout-with-uicollectionview/. For the lazy ones:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesForElementsInRect = super.layoutAttributesForElements(in: rect)
var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()
// use a value to keep track of left margin
var leftMargin: CGFloat = 0.0;
for attributes in attributesForElementsInRect! {
let refAttributes = attributes
// assign value if next row
if (refAttributes.frame.origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left
} else {
// set x position of attributes to current margin
var newLeftAlignedFrame = refAttributes.frame
newLeftAlignedFrame.origin.x = leftMargin
refAttributes.frame = newLeftAlignedFrame
}
// calculate new value for current margin
leftMargin += refAttributes.frame.size.width + 10
newAttributesForElementsInRect.append(refAttributes)
}
return newAttributesForElementsInRect
}
Swift 5 UIKit Programmatically
//Create UICollectionView
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
//CollectionCellView width autoSize
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
collectionView.backgroundColor = .clear
collectionView.delegate = self
collectionView.dataSource = self
collectionView.alwaysBounceHorizontal = true
//Add Spacing in each cell
layout.minimumInteritemSpacing = 15
layout.minimumLineSpacing = 15
return collectionView
}()
Try playing around with this method:
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
insetForSectionAtIndex:(NSInteger)section {
UIEdgeInsets insets = UIEdgeInsetsMake(?, ?, ?, ?);
return insets;
}

How to create a centered UICollectionView like in Spotify's Player

I am have a lot of difficulty trying to create a UICollectionView like in Spotify's Player that acts like this:
The problem for me is two fold.
1) How do I center the cells so that you can see the middle cell as well as the one of the left and right.
If I create cells that are square and add spacing between each cell, the cells are correctly displayed but are not centered.
2) With pagingEnabled = YES, the collectionview correctly swipes from one page to another. However, without the cells being centered, it simply moves the collection view over a page which is the width of the screen. So the question is how do you make the pages move so you get the effect above.
3) how do you animate the size of the cells as they move
I don't want to worry about this too much. If I can get that to work it would be great, but the harder problems are 1 and 2.
The code I have currently is a simple UICollectionView with normal delegate setup and custom UICollectionview cells that are squares. Maybe I neeed to subclass UICollectionViewFlowLayout? Or maybe I need to turn pagingEnabled to NO and then use custom swipe events? Would love any help!
In order to create an horizontal carousel layout, you'll have to subclass UICollectionViewFlowLayout then override targetContentOffset(forProposedContentOffset:withScrollingVelocity:), layoutAttributesForElements(in:) and shouldInvalidateLayout(forBoundsChange:).
The following Swift 5 / iOS 12.2 complete code shows how to implement them.
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let collectionDataSource = CollectionDataSource()
let flowLayout = ZoomAndSnapFlowLayout()
override func viewDidLoad() {
super.viewDidLoad()
title = "Zoomed & snapped cells"
guard let collectionView = collectionView else { fatalError() }
//collectionView.decelerationRate = .fast // uncomment if necessary
collectionView.dataSource = collectionDataSource
collectionView.collectionViewLayout = flowLayout
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
}
ZoomAndSnapFlowLayout.swift
import UIKit
class ZoomAndSnapFlowLayout: UICollectionViewFlowLayout {
let activeDistance: CGFloat = 200
let zoomFactor: CGFloat = 0.3
override init() {
super.init()
scrollDirection = .horizontal
minimumLineSpacing = 40
itemSize = CGSize(width: 150, height: 150)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
guard let collectionView = collectionView else { fatalError() }
let verticalInsets = (collectionView.frame.height - collectionView.adjustedContentInset.top - collectionView.adjustedContentInset.bottom - itemSize.height) / 2
let horizontalInsets = (collectionView.frame.width - collectionView.adjustedContentInset.right - collectionView.adjustedContentInset.left - itemSize.width) / 2
sectionInset = UIEdgeInsets(top: verticalInsets, left: horizontalInsets, bottom: verticalInsets, right: horizontalInsets)
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
// Make the cells be zoomed when they reach the center of the screen
for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {
let distance = visibleRect.midX - attributes.center.x
let normalizedDistance = distance / activeDistance
if distance.magnitude < activeDistance {
let zoom = 1 + zoomFactor * (1 - normalizedDistance.magnitude)
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1)
attributes.zIndex = Int(zoom.rounded())
}
}
return rectAttributes
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return .zero }
// Add some snapping behaviour so that the zoomed cell is always centered
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen
return true
}
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
CollectionDataSource.swift
import UIKit
class CollectionDataSource: NSObject, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 9
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
return cell
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .green
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Expected result:
Source:
WWDC 2012 session 219 - "Advanced Collection Views and Building Custom Layouts"
Well, I made UICollectionview moving just like this, yesterday.
I can share my code with you :)
Here's my storyboard
make sure uncheck 'Paging Enabled'
Here's my code.
#interface FavoriteViewController () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
{
NSMutableArray * mList;
CGSize cellSize;
}
#property (weak, nonatomic) IBOutlet UICollectionView *cv;
#end
#implementation FavoriteViewController
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// to get a size.
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
CGRect screenFrame = [[UIScreen mainScreen] bounds];
CGFloat width = screenFrame.size.width*self.cv.frame.size.height/screenFrame.size.height;
cellSize = CGSizeMake(width, self.cv.frame.size.height);
// if cell's height is exactly same with collection view's height, you get an warning message.
cellSize.height -= 1;
[self.cv reloadData];
// setAlpha is for hiding looking-weird at first load
[self.cv setAlpha:0];
}
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self scrollViewDidScroll:self.cv];
[self.cv setAlpha:1];
}
#pragma mark - scrollview delegate
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if(mList.count > 0)
{
const CGFloat centerX = self.cv.center.x;
for(UICollectionViewCell * cell in [self.cv visibleCells])
{
CGPoint pos = [cell convertPoint:CGPointZero toView:self.view];
pos.x += cellSize.width/2.0f;
CGFloat distance = fabs(centerX - pos.x);
// If you want to make side-cell's scale bigger or smaller,
// change the value of '0.1f'
CGFloat scale = 1.0f - (distance/centerX)*0.1f;
[cell setTransform:CGAffineTransformMakeScale(scale, scale)];
}
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{ // for custom paging
CGFloat movingX = velocity.x * scrollView.frame.size.width;
CGFloat newOffsetX = scrollView.contentOffset.x + movingX;
if(newOffsetX < 0)
{
newOffsetX = 0;
}
else if(newOffsetX > cellSize.width * (mList.count-1))
{
newOffsetX = cellSize.width * (mList.count-1);
}
else
{
NSUInteger newPage = newOffsetX/cellSize.width + ((int)newOffsetX%(int)cellSize.width > cellSize.width/2.0f ? 1 : 0);
newOffsetX = newPage*cellSize.width;
}
targetContentOffset->x = newOffsetX;
}
#pragma mark - collectionview delegate
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return mList.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"list" forIndexPath:indexPath];
NSDictionary * dic = mList[indexPath.row];
UIImageView * iv = (UIImageView *)[cell.contentView viewWithTag:1];
UIImage * img = [UIImage imageWithData:[dic objectForKey:kKeyImg]];
[iv setImage:img];
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return cellSize;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
CGFloat gap = (self.cv.frame.size.width - cellSize.width)/2.0f;
return UIEdgeInsetsMake(0, gap, 0, gap);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
Key code of make cell centered is
scrollViewWillEndDragging
insetForSectionAtIndex
Key code of animate the size is
scrollviewDidScroll
I wish this helps you
P.S.
If you want to change alpha just like the image that you uploaded, add [cell setalpha] in scrollViewDidScroll
As you have said in the comment you want that in the Objective-c code, there is a very famous library called iCarousel which can be helpful in completing your requirement.Link: https://github.com/nicklockwood/iCarousel
You may use 'Rotary' or 'Linear' or some other style with little or no modification to implement the custom view
To implement it you have implement only some delegate methods of it and it's working for ex:
//specify the type you want to use in viewDidLoad
_carousel.type = iCarouselTypeRotary;
//Set the following delegate methods
- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel
{
//return the total number of items in the carousel
return [_items count];
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
UILabel *label = nil;
//create new view if no view is available for recycling
if (view == nil)
{
//don't do anything specific to the index within
//this `if (view == nil) {...}` statement because the view will be
//recycled and used with other index values later
view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200.0f, 200.0f)];
((UIImageView *)view).image = [UIImage imageNamed:#"page.png"];
view.contentMode = UIViewContentModeCenter;
label = [[UILabel alloc] initWithFrame:view.bounds];
label.backgroundColor = [UIColor clearColor];
label.textAlignment = NSTextAlignmentCenter;
label.font = [label.font fontWithSize:50];
label.tag = 1;
[view addSubview:label];
}
else
{
//get a reference to the label in the recycled view
label = (UILabel *)[view viewWithTag:1];
}
//set item label
label.text = [_items[index] stringValue];
return view;
}
- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
if (option == iCarouselOptionSpacing)
{
return value * 1.1;
}
return value;
}
You can check the full working demo from 'Examples/Basic iOS Example' which is included with the Github repository link
As it is old and popular you can find some related tutorials for it and it will also be much stable than the custom code implementation
I wanted similar behavior a little while back, and with the help of #Mike_M I was able to figure it out. Although there are many, many way to do this, this particular implementation is to create a custom UICollectionViewLayout.
Code below(gist can be found here: https://gist.github.com/mmick66/9812223)
Now it's important to set the following: *yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast, this prevents cells being skipped by a quick swipe.
That should cover part 1 and 2. Now, for part 3 you could incorporate that in the custom collectionView by constantly invalidating and updating, but it's a bit of a hassle if you ask me. So another approach would be to to set a CGAffineTransformMakeScale( , ) in the UIScrollViewDidScroll where you dynamically update the cell's size based on it's distance from the center of the screen.
You can get the indexPaths of the visible cells of the collectionView using [*youCollectionView indexPathsForVisibleItems] and then getting the cells for these indexPaths. For every cell, calculate the distance of its center to the center of yourCollectionView
The center of the collectionView can be found using this nifty method: CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];
Now set up a rule, that if the cell's center is further than x away, the size of the cell is for example the 'normal size', call it 1. and the closer it gets to the center, the closer it gets to twice the normal size 2.
then you can use the following if/else idea:
if (distance > x) {
cell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
cell.transform = CGAffineTransformMakeScale(scale, scale);
}
What happens is that the cell's size will exactly follow your touch. Let me know if you have any more questions as I'm writing most of this out of the top of my head).
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset
withScrollingVelocity:(CGPoint)velocity {
CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;
NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];
UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in attributesArray) {
// == Skip comparison with non-cell items (headers and footers) == //
if (attributes.representedElementCategory !=
UICollectionElementCategoryCell) {
continue;
}
// == First time in the loop == //
if(!candidateAttributes) {
candidateAttributes = attributes;
continue;
}
if (fabsf(attributes.center.x - proposedContentOffsetCenterX) <
fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
candidateAttributes = attributes;
}
}
return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);
}
pagingEnabled should not be enabled as it needs each cell to be the width of you view which will not work for you since you need to see the edges of other cells. For your points 1 and 2. I think you'll find what you need here from one of my late answers to another question.
The animation of the cell sizes can be achieved by subclassing UIcollectionviewFlowLayout and overriding layoutAttributesForItemAtIndexPath: Within that modify the layout attributes provided by first calling super and then modify the layout attributes size based on the position as it relates to the window centre.
Hopefully this helps.
If you want to have uniform spacing between cells you can replace the following method in ZoomAndSnapFlowLayout from Imanou Petit's solution:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
let visibleAttributes = rectAttributes.filter { $0.frame.intersects(visibleRect) }
// Keep the spacing between cells the same.
// Each cell shifts the next cell by half of it's enlarged size.
// Calculated separately for each direction.
func adjustXPosition(_ toProcess: [UICollectionViewLayoutAttributes], direction: CGFloat, zoom: Bool = false) {
var dx: CGFloat = 0
for attributes in toProcess {
let distance = visibleRect.midX - attributes.center.x
attributes.frame.origin.x += dx
if distance.magnitude < activeDistance {
let normalizedDistance = distance / activeDistance
let zoomAddition = zoomFactor * (1 - normalizedDistance.magnitude)
let widthAddition = attributes.frame.width * zoomAddition / 2
dx = dx + widthAddition * direction
if zoom {
let scale = 1 + zoomAddition
attributes.transform3D = CATransform3DMakeScale(scale, scale, 1)
}
}
}
}
// Adjust the x position first from left to right.
// Then adjust the x position from right to left.
// Lastly zoom the cells when they reach the center of the screen (zoom: true).
adjustXPosition(visibleAttributes, direction: +1)
adjustXPosition(visibleAttributes.reversed(), direction: -1, zoom: true)
return rectAttributes
}

Resources