109

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.

Cœur
  • 32,421
  • 21
  • 173
  • 232
Damien
  • 1,445
  • 2
  • 14
  • 16
  • Possible duplicate of [how do you determine spacing between cells in UICollectionView flowLayout](https://stackoverflow.com/questions/13017257/how-do-you-determine-spacing-between-cells-in-uicollectionview-flowlayout) – Cœur Jun 29 '17 at 15:15

22 Answers22

195

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;
}
Tristan Newman
  • 324
  • 1
  • 4
  • 12
Angel G. Olloqui
  • 7,410
  • 3
  • 28
  • 30
  • 4
    Thanks! I was getting a warning, which is fixed by making a copy of the attributes in the array `let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }` – tkuichooseyou May 17 '16 at 18:29
  • 3
    For anyone who may have stumbled upon this looking for a center-aligned version as I did, I've posted a modified version of Angel's answer here: http://stackoverflow.com/a/38254368/2845237 – Alex Koshy Jul 07 '16 at 20:10
  • I tried this solution but some of the cell that's on the right of the collection view exceeding the bounds of the collection view. Did anyone else encounter this? – Happiehappie Oct 31 '16 at 01:31
  • @Happiehappie yes i have that behavior as well. any fix? – John Baum Nov 11 '16 at 10:13
  • 1
    Tried this solution but couldn't associate the layout with the collectionView in the Interface Builder Utilies panel. Ended up doing it by code. Any clue why? – acrespo Jan 14 '17 at 02:42
  • Also, this disregards the left Section Inset set for the uiCollectionView – acrespo Jan 14 '17 at 04:33
  • This solution seems to remove any spacing between the cells - even if its set in the interface builder- how do you preserve spacing of 10 ? – UKDataGeek Apr 08 '17 at 09:56
  • @Angle we face issue in supplementary view in iphone X device. supplementary is not display if we use above solution. Please help – chirag shah Nov 08 '17 at 10:18
  • https://stackoverflow.com/a/33172179/1754401 fixed warning about not copying attributes in objective-c for me – tagy22 Jul 10 '18 at 09:55
  • Watchit: This solution doesn't implement `layoutAttributesForItem(at:)`. Docs say it must be implemented (and return the correct attributes for any given item). It may just happen that in _some cases_ this isn't an observable problem, but of course there's no guarantee. I suspect trouble. – wardw Aug 03 '18 at 16:59
  • 1
    How do you make this align to the left in left-to-right systems, and to the right in right-to-left systems? – Rick Apr 22 '19 at 13:21
  • Would you make it for right alignment https://stackoverflow.com/questions/56349052/aligning-right-to-left-on-uicollectionview-with-contents/ – Nazmul Hasan May 29 '19 at 17:16
  • I need this but in a scrollDirection = .horizontal, can you please tell me how? Thanks! – Refael.S Jul 01 '19 at 10:36
  • 4
    For usage do this self.collectionView.collectionViewLayout = LeftAlignedCollectionViewFlowLayout() – blackops Oct 22 '19 at 20:31
  • This is great and worked for me. One issue is that UICollectionViewLayout is respecting the `UICollectionViewDelegateFlowLayout` and this `LeftAlignedCollectionViewFlowLayout` does not (for some reason)... There are many ways to work around this, of course. – Dan Rosenstark Nov 04 '19 at 12:01
  • Thanks!! I spent my entire morning trying to figure this out. – tgrable Jan 15 '20 at 19:30
  • Why not call viewForSupplementaryElementOfKind at first render? But referenceSizeForHeaderInSection is run ok. Everyone has same issue? – Kakashi Dec 21 '20 at 04:34
69

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:

Left-aligned layout

or ➡︎ right:

Right-aligned layout

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.

Mischa
  • 12,670
  • 7
  • 51
  • 94
  • I apply with this scenario https://stackoverflow.com/questions/56349052/aligning-right-to-left-on-uicollectionview-with-contents/ but it's not working. would you tell me how can i achieve this – Nazmul Hasan May 29 '19 at 17:22
  • Even with this (which is a really awesome project) I experience flashing in my apps vertical scrolling collection view. I have 20 cells, and on the first scroll, the first cell is blank and all the cells are shifted one position to the right. After one full scroll up and down, things align properly. – Parth Tamane Sep 01 '19 at 05:08
  • Awesome project! It would be great to see it enabled for Carthage. – pjuzeliunas Dec 04 '19 at 14:52
32

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:

enter image description here


#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:

enter image description here

Imanou Petit
  • 76,586
  • 23
  • 234
  • 201
31

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

enter image description here

And finally...

Give thanks to @AlexShubin above who first clarified this!

Fattie
  • 30,632
  • 54
  • 336
  • 607
  • Hey. I'm a little bit confused. Adding space for every string at the start and finish still shows my word in item cell w/o the space I've added. Any idea of how to do it? – Mohamed Lee Dec 11 '19 at 11:46
  • 1
    If you have section headers in your collection view this seems to kill them. – TylerJames Feb 20 '20 at 17:52
  • @Fattie - Can I ask one question related to tag collectionView İtems please? – Tekhe Mar 18 '21 at 14:34
  • @Tekhe - yes of course! (You can also ask it as a Question and make a link here.) – Fattie Mar 18 '21 at 15:46
22

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.

Wex
  • 3,420
  • 2
  • 24
  • 44
Mike Sand
  • 2,650
  • 12
  • 22
  • I think this is way better than Chris Wagner's solution (http://stackoverflow.com/a/15554667/482557) on the linked question—your implementation has zero repetitive UICollectionViewLayout calls. – Evan R Jun 15 '15 at 05:10
  • 1
    Doesn't look like this works if the row has a single item because UICollectionViewFlowLayout centers it. You can check the center to solve the issue. Something like `attribute.center.x == self.collectionView?.bounds.midX` – Ryan Poolos Jul 20 '15 at 21:16
  • I implemented this, but for some reason it breaks for the first section: The first cell appears on the left of the first row, and the second cell (which does not fit on the same row) goes to the next row, but not left aligned. Instead, its x position starts where the first cell ends. – Nicolas Miari Oct 07 '15 at 06:31
  • @NicolasMiari The loop decides it's a new row and resets the `leftMargin` with the `if (attributes.frame.origin.x == self.sectionInset.left) {` check. This assumes that the default layout has aligned the new row's cell to be flush with the `sectionInset.left`. If this is not the case, the behavior you describe would result. I would insert a breakpoint inside the loop and check both sides for the second row's cell just before the comparison. Best of luck. – Mike Sand Oct 07 '15 at 17:27
  • 3
    Thanks... I ended up using UICollectionViewLeftAlignedLayout: https://github.com/mokagio/UICollectionViewLeftAlignedLayout. It overrides a couple more methods. – Nicolas Miari Oct 07 '15 at 23:51
  • Looks like this is breaking with `estimatedItemSize` set on the layout. See answer below if you need autosizing cells. – Ryan Poolos Mar 02 '16 at 15:37
  • Best solution I found: https://github.com/Digipolitan/collection-view-left-align-flow-layout – d4Rk Oct 18 '17 at 13:21
10

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];
mklb
  • 987
  • 9
  • 8
  • Missing an opening square bracket in your initialisation of the layout there – RyanOfCourse Nov 05 '15 at 16:52
  • 1
    I tested the two answers https://github.com/eroth/ERJustifiedFlowLayout and UICollectionViewLeftAlignedLayout. Only the second one produced the right result when using estimatedItemSize (self-sizing). – EckhardN Dec 16 '15 at 14:18
7

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.

enter image description here

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.

MkVal
  • 834
  • 1
  • 10
  • 15
6

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
    }
}
Cem Schemel
  • 305
  • 1
  • 9
Ryan Poolos
  • 17,727
  • 4
  • 61
  • 95
  • This works for first section however label doesn't resize on the second section until table scrolls. My collection view is in the table cell. – EI Captain v2.0 Jan 23 '19 at 12:48
5

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

Community
  • 1
  • 1
Evan R
  • 793
  • 11
  • 25
5

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
}
Cem Schemel
  • 305
  • 1
  • 9
GregP
  • 1,365
  • 15
  • 16
5

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.

niku
  • 396
  • 2
  • 11
5

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)
    }
}
gmogames
  • 2,743
  • 1
  • 23
  • 36
cseh_17
  • 63
  • 1
  • 5
  • That idea works if there is less than one row of cells, but not when any section has a number of items that is not evenly divisible by the row size. – Robin Daugherty Apr 18 '21 at 01:35
4

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
}
Cem Schemel
  • 305
  • 1
  • 9
kotvaska
  • 569
  • 5
  • 11
3

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
    }
}
EricSchaefer
  • 22,338
  • 20
  • 63
  • 99
Alex Shubin
  • 3,111
  • 1
  • 23
  • 31
2

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;
}
Community
  • 1
  • 1
Shu Zhang
  • 451
  • 10
  • 18
2

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
    }
}
1

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
    }
}
Youstanzr
  • 436
  • 1
  • 6
  • 12
0

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
Dharmesh Porwal
  • 1,418
  • 2
  • 12
  • 20
Anas iqbal
  • 970
  • 5
  • 21
0

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;
}
Lal Krishna
  • 12,476
  • 4
  • 53
  • 65
0

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
}
Cem Schemel
  • 305
  • 1
  • 9
Radek
  • 61
  • 1
  • 8
0

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
    }
}
Pang
  • 8,605
  • 144
  • 77
  • 113
Naresh
  • 693
  • 5
  • 15
0

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;
}

Here's what the final collectionview (with custom cell widths) ends up looking like: collectionview