How to animate collection view layout change while using `layoutAttributesForElements`? - ios

I made a custom collection view flow layout that can toggle (with animation) between "film-strip" and "list" layouts. But after adding some fancy animations to the edge cells, the toggle animation broke. Here's what it looks like currently, without those changes:
The animation is nice and smooth, right? Here's the current, working code (full demo project here):
enum LayoutType {
case strip
case list
}
class FlowLayout: UICollectionViewFlowLayout {
var layoutType: LayoutType
var layoutAttributes = [UICollectionViewLayoutAttributes]() /// store the frame of each item
var contentSize = CGSize.zero /// the scrollable content size of the collection view
override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
/// pass attributes to the collection view flow layout
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutAttributes[indexPath.item]
}
// MARK: - Problem is here
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
/// edge cells don't shrink, but the animation is perfect
return layoutAttributes.filter { rect.intersects($0.frame) } /// try deleting this line
/// edge cells shrink (yay!), but the animation glitches out
return shrinkingEdgeCellAttributes(in: rect)
}
/// makes the edge cells slowly shrink as you scroll
func shrinkingEdgeCellAttributes(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = layoutAttributes.filter { rect.intersects($0.frame) }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size) /// rect of the visible collection view cells
let leadingCutoff: CGFloat = 50 /// once a cell reaches here, start shrinking it
let trailingCutoff: CGFloat
let paddingInsets: UIEdgeInsets /// apply shrinking even when cell has passed the screen's bounds
if layoutType == .strip {
trailingCutoff = CGFloat(collectionView.bounds.width - leadingCutoff)
paddingInsets = UIEdgeInsets(top: 0, left: -50, bottom: 0, right: -50)
} else {
trailingCutoff = CGFloat(collectionView.bounds.height - leadingCutoff)
paddingInsets = UIEdgeInsets(top: -50, left: 0, bottom: -50, right: 0)
}
for attributes in rectAttributes where visibleRect.inset(by: paddingInsets).contains(attributes.center) {
/// center of each cell, converted to a point inside `visibleRect`
let center = layoutType == .strip
? attributes.center.x - visibleRect.origin.x
: attributes.center.y - visibleRect.origin.y
var offset: CGFloat?
if center <= leadingCutoff {
offset = leadingCutoff - center /// distance from the cutoff, 0 if exactly on cutoff
} else if center >= trailingCutoff {
offset = center - trailingCutoff
}
if let offset = offset {
let scale = 1 - (pow(offset, 1.1) / 200) /// gradually shrink the cell
attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
return rectAttributes
}
/// initialize with a LayoutType
init(layoutType: LayoutType) {
self.layoutType = layoutType
super.init()
}
/// make the layout (strip vs list) here
override func prepare() { /// configure the cells' frames
super.prepare()
guard let collectionView = collectionView else { return }
var offset: CGFloat = 0 /// origin for each cell
let cellSize = layoutType == .strip ? CGSize(width: 100, height: 50) : CGSize(width: collectionView.frame.width, height: 50)
for itemIndex in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: itemIndex, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let origin: CGPoint
let addedOffset: CGFloat
if layoutType == .strip {
origin = CGPoint(x: offset, y: 0)
addedOffset = cellSize.width
} else {
origin = CGPoint(x: 0, y: offset)
addedOffset = cellSize.height
}
attributes.frame = CGRect(origin: origin, size: cellSize)
layoutAttributes.append(attributes)
offset += addedOffset
}
self.contentSize = layoutType == .strip /// set the collection view's `collectionViewContentSize`
? CGSize(width: offset, height: cellSize.height) /// if strip, height is fixed
: CGSize(width: cellSize.width, height: offset) /// if list, width is fixed
}
/// boilerplate code
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 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
}
}
class ViewController: UIViewController {
var data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
var isExpanded = false
lazy var listLayout = FlowLayout(layoutType: .list)
lazy var stripLayout = FlowLayout(layoutType: .strip)
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
#IBAction func toggleExpandPressed(_ sender: Any) {
isExpanded.toggle()
if isExpanded {
collectionView.setCollectionViewLayout(listLayout, animated: true)
} else {
collectionView.setCollectionViewLayout(stripLayout, animated: true)
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = stripLayout /// start with the strip layout
collectionView.dataSource = self
collectionViewHeightConstraint.constant = 300
}
}
/// sample data source
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath) as! Cell
cell.label.text = "\(data[indexPath.item])"
cell.contentView.layer.borderWidth = 5
cell.contentView.layer.borderColor = UIColor.red.cgColor
return cell
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
}
Again, everything works perfectly, including the animation. So then, I tried to make the cells shrink as they neared the screen's edge. I overrode layoutAttributesForElements to do this.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributes.filter { rect.intersects($0.frame) } /// delete this line
return shrinkingEdgeCellAttributes(in: rect) /// replace with this
}
Film-strip
List
The scale/shrink animation is great. However, when I toggle between the layouts, the transition animation is broken.
Before (return layoutAttributes.filter...)
After (return shrinkingEdgeCellAttributes(in: rect))
How can I fix this animation? Should I be using a custom UICollectionViewTransitionLayout, and if so, how?

Whew! This was a workout. I was able to modify your FlowLayout so that there are no hiccups in animation. See below.
It works!
Problem
This is what was happening. When you change layouts, the layoutAttributesForElements method in FlowLayout is called twice if the content offset of the collection view is anything but (0, 0).
This is because you have overridden 'shouldInvalidateLayout' to return true regardless of whether it is actually needed. I believe the UICollectionView calls this method on the layout before and after the layout change (as per the observation).
The side effect of this is that your scale transform is applied twice - before and after the animations to the visible layout attributes.
Unfortunately, the scale transform is applied based on the contentOffset of the collection view (link)
let visibleRect = CGRect(
origin: collectionView.contentOffset,
size: collectionView.frame.size
)
During layout changes the contentOffset is not consistent. Before the animation starts contentOffset is applicable to the previous layout. After the animation, it is relative to the new layout. Here I also noticed that without a good reason, the contentOffset "jumps" around (see note 1)
Since you use the visibleRect to query the set of Layout Attributes to apply the scale on, it introduces further errors.
Solution
I was able to find a solution by applying these changes.
Write helpers methods to transform the content offset (and dependent visibleRect) left by the previous layout to values meaningful for this layout.
Prevent redundant layout attribute calculates in prepare method
Track when and when not the layout is animating
// In Flow Layout
class FlowLayout: UICollectionViewFlowLayout {
var animating: Bool = false
// ...
}
// In View Controller,
isExpanded.toggle()
if isExpanded {
listLayout.reset()
listLayout.animating = true // <--
// collectionView.setCollectionViewLayout(listLayout)
} else {
stripLayout.reset()
stripLayout.animating = true // <--
// collectionView.setCollectionViewLayout(stripLayout)
}
Override targetContentOffset method to handle content offset changes (prevent jumps)
// In Flow Layout
class FlowLayout: UICollectionViewFlowLayout {
var animating: Bool = false
var layoutType: LayoutType
// ...
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard animating else {
// return super
}
// Use our 'graceful' content content offset
// instead of arbitrary "jump"
switch(layoutType){
case .list: return transformCurrentContentOffset(.fromStripToList)
case .strip: return transformCurrentContentOffset(.fromListToStrip)
}
}
// ...
The implementation of content offset transforming is as follows.
/**
Transforms this layouts content offset, to the other layout
as specified in the layout transition parameter.
*/
private func transformCurrentContentOffset(_ transition: LayoutTransition) -> CGPoint{
let stripItemWidth: CGFloat = 100.0
let listItemHeight: CGFloat = 50.0
switch(transition){
case .fromStripToList:
let numberOfItems = collectionView!.contentOffset.x / stripItemWidth // from strip
var newPoint = CGPoint(x: 0, y: numberOfItems * CGFloat(listItemHeight)) // to list
if (newPoint.y + collectionView!.frame.height) >= contentSize.height{
newPoint = CGPoint(x: 0, y: contentSize.height - collectionView!.frame.height)
}
return newPoint
case .fromListToStrip:
let numberOfItems = collectionView!.contentOffset.y / listItemHeight // from list
var newPoint = CGPoint(x: numberOfItems * CGFloat(stripItemWidth), y: 0) // to strip
if (newPoint.x + collectionView!.frame.width) >= contentSize.width{
newPoint = CGPoint(x: contentSize.width - collectionView!.frame.width, y: 0)
}
return newPoint
}
}
There are some minor details I left out in the comments and as a pull request to OP's demo project so anyone interested can study it.
The key take-aways are,
Use targetContentOffset when arbitrary changes in content offset occur in response to layout changes.
Be careful about incorrect query of layout attributes in layoutAttributesForElements. Debug your rects!
Remember to clear your cached layout attributes on the prepare() method.
Notes
The "jump" behavior is evident even before you introduced scale transforms as seen in your gif.
I sincerely apologize if the answer is lengthy. Or, The solution is not quite what you wanted. The question was interesting which is why I spent the whole day trying to find a way to help.
Fork and Pull request.

Thanks for your detailed investigation #Thisura Dodangoda
– it was instrumental in helping me solve a similar problem. For folks who end up here, I want to add a tiny detail in case you run into another issue that I did.
The UICollectionViewLayout API has 2 very similar methods:
func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint
This method Retrieves the point at which to stop scrolling
and
func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint
This method Retrieves the content offset to use after an animated layout update or change
I had already implemented the first for some custom behaviour during scrolling, and I was trying to implement the solution posted by #Thisura Dodangoda in that method.
However, these are used for completely different purposes. You need to use the second method (without the velocity parameter) to implement the solution for layout changes.

Related

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!!!

UICollectionView Custom Layout Middle column starting higher

I have a UICollectionView scrolling vertically right now. I would like to have it look like this =, where the middle row starts a little higher than the other two to create a cool and interesting effect.
Any ideas on how to achieve this?
The Collection view layout can be achieved by creating a custom layout class.
Theory:
Basically, collection view works directly with the custom layout object to manage the overall layout process, asking for the required layout information.
During the layout process, the collection view calls specific methods of the layout object. These methods provide a chance to calculate the position of items and to provide the collection view with the primary information it needs.
Following methods are always called in order during the layout process:
prepare(): Perform the up-front calculations needed to provide layout information
collectionViewContentSize: Return the overall size of the entire content area based on your initial calculations
layoutAttributesForElements(in:): Return the attributes for cells and views that are in the specified rectangle
Practical:
Prerequisite: Assuming we have a Collection view in place and configured with the
datasource and delegates, let's create a UICollectionViewLayout
subclass. I named it HiveLayout. I also assigned it to the
collectionView in the storyboard. We also need some variable that will be useful in the process
// Properties for configuring the layout: the number of columns and the cell padding.
fileprivate var numberOfColumns = 3
fileprivate var cellPadding: CGFloat = 10
// Cache the calculated attributes. When you call prepare(), you’ll calculate the attributes for all items and add them to the cache. You can be efficient and query the cache instead of recalculating them every time.
fileprivate var cache = [UICollectionViewLayoutAttributes]()
// Properties to store the content size.
fileprivate var contentHeight: CGFloat = 0
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
prepare(): This is where we will actually calculate the attributes of the cells
override func prepare() {
// If cache is empty and the collection view exists – calculate the layout attributes
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// xOffset: array with the x-coordinate for every column based on the column widths
// yOffset: array with the y-position for every column, Using odd-even logic to push the even cell upwards and odd cells down.
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset = [CGFloat]()
for column in 0 ..< numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset = [CGFloat]()
for i in 0..<numberOfColumns {
yOffset.append((i % 2 == 0) ? (columnWidth / 2) : 0)
}
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
// Calculate insetFrame that can be set to the attribute
let cellHeight = columnWidth - (cellPadding * 2)
let height = cellPadding * 2 + cellHeight
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// Create an instance of UICollectionViewLayoutAttribute, sets its frame using insetFrame and appends the attributes to cache.
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// Update the contentHeight to account for the frame of the newly calculated item. It then advances the yOffset for the current column based on the frame
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
collectionViewContentSize:
// Using contentWidth and contentHeight, calculate collectionViewContentSize.
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
layoutAttributesForElements(in:):
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
layoutAttributesForItem(at:): To return attributes for particular cell
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
Please check out the gist for the Layout Class.
Here it is in action!!

Swift: paging by screen doesn't work for two column layout collection view

I have a collectionview controller with custom 2-columns layout:
class CVLayout: UICollectionViewFlowLayout {
override init() {
super.init()
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLayout()
}
override var itemSize: CGSize {
set {
}
get {
let numberOfColumns: CGFloat = 2
let itemWidth = (self.collectionView!.frame.width - (numberOfColumns - 1)) / numberOfColumns
return CGSize(width: itemWidth, height: itemWidth)
}
}
func setupLayout() {
minimumInteritemSpacing = 1
minimumLineSpacing = 1
scrollDirection = .horizontal
}
}
In my controller I set
class ViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
collectionView?.isScrollEnabled = true
collectionView?.isPagingEnabled = true
}
I have 7 items, 6 items are displayed on the screen and I expect to see one item on the next page, but instead I see 4 (3 from the previous page)
I tried to play with number of sections and items in section, but it doesn't help. What I've found from other topics is that default paging should be by screen, not by cell (in my case a column). What am I doing wrong?
If I understand your question, I do not think you are doing anything wrong per se. The content size of the collection view is determined (in part) by the item size. Right now that is approximately the width of 3 items (or a page and a half of content). I think you need to override the collection view's content size property to reflect full pages of content (in width) to achieve the desired effect.
override var collectionViewContentSize: CGSize {
if let collectionView = collectionView {
let numberOfItems = collectionView.numberOfItems(inSection: 0)
let pages = numberOfItems/2
let size = CGSize(width: collectionView.frame.width * CGFloat(pages), height: collectionView.frame.height)
return size
}
return CGSize(width: 0, height: 0)
}
Please see this answer How to expand UICollectionView contentSize when paging enable?. It is a bit old but I think it is trying to solve the same issue.
First of all UICollectionViewFlowLayout doesn't support paging
There are two solution that i think
first
use UIScrollView add 7 items simply with paging option
second
use UICollectionView modifying your UICollectionViewFlowLayout
subclass UICollectionViewFlowLayout to support paging
ex)
In Swift3
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
you can find more information searching UICollectionViewFlowLayout with Paging

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...

Left Align Cells in UICollectionView

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

Resources