50

I have a UICollectionView with a flow layout and each cell is a square. How do I determine the spacing between each cells on each row? I can't seem to find the appropriate settings for this. I see there's a min spacing attributes on the nib file for a collection view, but I set this to 0 and the cells doesn't even stick.

enter image description here

Any other idea?

Cœur
  • 32,421
  • 21
  • 173
  • 232
adit
  • 28,968
  • 65
  • 214
  • 354
  • see also https://stackoverflow.com/questions/22539979/left-align-cells-in-uicollectionview – Cœur Jun 29 '17 at 15:17

12 Answers12

74

Update: Swift version of this answer: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift


Taking @matt's lead I modified his code to insure that items are ALWAYS left aligned. I found that if an item ended up on a line by itself, it would be centered by the flow layout. I made the following changes to address this issue.

This situation would only ever occur if you have cells that vary in width, which could result in a layout like the following. The last line always left aligns due to the behavior of UICollectionViewFlowLayout, the issue lies in items that are by themselves in any line but the last one.

With @matt's code I was seeing.

enter image description here

In that example we see that cells get centered if they end up on the line by themselves. The code below insures your collection view would look like this.

enter image description here

#import "CWDLeftAlignedCollectionViewFlowLayout.h"

const NSInteger kMaxCellSpacing = 9;

@implementation CWDLeftAlignedCollectionViewFlowLayout

- (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];

    CGRect currentFrame = currentItemAttributes.frame;

    if (indexPath.item == 0) { // first item of section
        currentFrame.origin.x = sectionInset.left; // first item of the section should always be left aligned
        currentItemAttributes.frame = currentFrame;

        return currentItemAttributes;
    }

    NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
    CGFloat previousFrameRightPoint = CGRectGetMaxX(previousFrame) + kMaxCellSpacing;

    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
        currentFrame.origin.x = sectionInset.left; // first item on the line should always be left aligned
        currentItemAttributes.frame = currentFrame;
        return currentItemAttributes;
    }

    currentFrame.origin.x = previousFrameRightPoint;
    currentItemAttributes.frame = currentFrame;
    return currentItemAttributes;
}

@end
Iulian Onofrei
  • 7,489
  • 8
  • 59
  • 96
Chris Wagner
  • 20,295
  • 8
  • 70
  • 94
  • 1
    Off-topic question, how do I implement a tag UI element like yours, any open source project to suggest? Thanks – X.Y. Jun 20 '13 at 02:41
  • Use a UIButton without a target (or with a target in case you need one), there are lots of custom UIButtons on GitHub, just search for "Button" in the Objective-C category – GangstaGraham Jun 20 '13 at 06:16
  • 1
    What you are seeing in my screenshot are `UICollectionViewCell`s within a `UICollectionView`. The touch events are handled by the delegate methods. Then they are styled accordingly just using a stretchable background, aside from the blue one, that is just an image within a cell. No open source stuff from what I am aware of but this is pretty simple with a `UICollectionView`, the hard part was getting them to align this way, and that is done for you here ;) – Chris Wagner Jun 20 '13 at 16:30
  • Thanks so much for this! It was incredibly helpful and saved me a ton of time. I was wondering though if you have any advice on how to make this flow horizontally as I am currently putting UICollectionViews in UITableViewCells and the vertical direction is being intercepted (and wouldn't be good ux anyway). – AdamG Aug 01 '13 at 03:14
  • Not sure I completely understand what you mean, maybe provide a mockup? – Chris Wagner Aug 01 '13 at 03:59
  • @ChrisWagner Hey, an offtopic, what font have you used for those tags? – jasonIM Aug 22 '13 at 21:59
  • @ChrisWagner can you provide some details about setting the rows distance? I've changed my `kMaxCellSpacing` to 3, implemented `- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath` which returns calculated size of tag buttons (yeah, i'm making same thing as at your's screenshot). But, I'm still having trouble with row spacing. When the distance between cells is 3, the distance between rows still about 10. Can you tell me, how I can affect it, please? – kokoko Dec 09 '13 at 15:42
  • @kokoko I am not sure why it's not respecting your value, have you tried a negative value to see if it has any affect? Obviously it'd be better to find the root cause but it's worth seeing if it is doing anything. I haven't done any custom collection view layouts since this so I am having a hard time recalling all of the intricacies. – Chris Wagner Dec 09 '13 at 17:47
  • @ChrisWagner actually, I am not having trouble with spacing between cells (tags), but I don't know how to change the spacing between rows, when cells are out of bounds and logics put them on a new **line**. Sorry, if you misunderstood me, I messed up row and line :) – kokoko Dec 10 '13 at 09:18
  • Nice! Many thanks to you and @matt for this, it definitely saved me a lot of time, and made me understand what was going on better! Cheers! I found an edge usage case: when the developer uses the delegate to set the insets for the section. To cover that case we need to first ask the delegate, and only in case it doesn't set the insets using the layout class ones. – mokagio Feb 07 '14 at 17:10
  • 1
    See here for a possible improvement to the code: http://stackoverflow.com/review/suggested-edits/4003993 – Robert Harvey Feb 07 '14 at 17:20
  • I added an improvement suggestion aswell: add rounding to previousFrameRightPoint or the content of the cells might be blurry. – eyeballz Aug 06 '14 at 14:00
  • Instead of kMaxCellSpacing you should use self.minimumInterimSpacing. – Victor Engel Apr 23 '15 at 04:07
  • To avoid the significant performance hit on large collections, I think a simple modification to this would be to simply cache the attributes and last indexPath. Then, at the beginning, simply check if [indexPath isEqual:self.cachedIndexPath] and if so, just use the cached attributes. Of course, you also need to update the cache when the attributes are computed otherwise. I've tested this in a sample project, and it seems to work well. I can't think of a scenario that would break this. If you can, please post. – Victor Engel Apr 27 '15 at 22:37
32

To get a maximum interitem spacing, subclass UICollectionViewFlowLayout and override layoutAttributesForElementsInRect: and layoutAttributesForItemAtIndexPath:.

For example, a common problem is this: the rows of a collection view are right-and-left justified, except for the last line which is left-justified. Let's say we want all the lines to be left-justified, so that the space between them is, let's say, 10 points. Here's an easy way (in your UICollectionViewFlowLayout subclass):

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray* arr = [super layoutAttributesForElementsInRect:rect];
    for (UICollectionViewLayoutAttributes* atts in arr) {
        if (nil == atts.representedElementKind) {
            NSIndexPath* ip = atts.indexPath;
            atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame;
        }
    }
    return arr;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes* atts =
    [super layoutAttributesForItemAtIndexPath:indexPath];

    if (indexPath.item == 0) // degenerate case 1, first item of section
        return atts;

    NSIndexPath* ipPrev =
    [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];

    CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame;
    CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10;
    if (atts.frame.origin.x <= rightPrev) // degenerate case 2, first item of line
        return atts;

    CGRect f = atts.frame;
    f.origin.x = rightPrev;
    atts.frame = f;
    return atts;
}

The reason this is so easy is that we aren't really performing the heavy lifting of the layout; we are leveraging the layout work that UICollectionViewFlowLayout has already done for us. It has already decided how many items go in each line; we're just reading those lines and shoving the items together, if you see what I mean.

matt
  • 447,615
  • 74
  • 748
  • 977
  • Just a question since I don't have the possibility to test your code right now. Will this answer help me fix the problem I'm having here? http://stackoverflow.com/questions/13411609/pack-collectionviewcells-tightly-together-on-ios – Joakim Engstrom Nov 22 '12 at 11:28
  • 1
    Awesome, added the vertical version of the code here: http://stackoverflow.com/questions/14482882/display-images-in-a-uicollectionview-how-to-achieve-fixed-vertical-spacing-bet/14542269#14542269 Thanks for the awesome code – Rob R. Jan 26 '13 at 22:35
  • 2
    Thanks for the solution. A minor point: the recursion within this code may be expensive if there's a collection view with a large set of horizontal items per row (collectionView.contentSize.width is much larger than cell width). – ricosrealm Apr 01 '15 at 01:34
16

There are a few things to consider:

  1. Try changing the minimum spacing in IB, but leave the cursor in that field. Notice that Xcode doesn't immediately mark the document as changed. When you click in a different field, though, Xcode does notice that the document is changed and marks it so in the file navigator. So, be sure to tab or click over to a different field after making a change.

  2. Save your storyboard/xib file after making a change, and be sure to rebuild the app. It's not hard to miss that step, and then you're left scratching your head wondering why your changes didn't seem to have any effect.

  3. UICollectionViewFlowLayout has a minimumInteritemSpacing property, which is what you're setting in IB. But the collection's delegate can also have a method to determine the inter-item spacing. That method trump's the layout's property, so if you implement it in your delegate your layout's property won't be used.

  4. Remember that the spacing there is a minimum spacing. The layout will use that number (whether it comes from the property or from the delegate method) as the smallest allowable space, but it may use a larger space if it has space leftover on the line. So if, for example, you set the minimum spacing to 0, you may still see a few pixels between items. If you want more control over exactly how the items are spaced you should probably use a different layout (possibly one of your own creation).

Caleb
  • 120,112
  • 19
  • 171
  • 259
  • 2
    Thanks for the awesome answer. I think what I am looking more is the maximumInteritemSpacing, is there such value? – adit Oct 22 '12 at 19:40
  • 2
    No, there isn't a max value. If you think about what the flow layout does, you'll see why. The flow layout puts as many items on a line as can fit while still respecting the minimum item spacing. At that point, there will be *n* pixels left over, where *n* is greater than or equal to 0 and less than the size of the next item plus the minimum spacing. The layout then takes those n pixels and distributes them to space the items on the row evenly. It couldn't do that if you set a max spacing. You can, however, write your own layout to space items however you like. – Caleb Oct 22 '12 at 19:54
  • +1, You can set `minimumInteritemSpacing` and `minimumLineSpacing` as well, depending on what you are trying to achieve. – iDev Feb 15 '13 at 23:09
  • Can you please identify the exact method in the layout's delegate that can determine horizontal inter-item spacing. – Drux Jun 01 '14 at 19:36
  • 1
    @Drux Sorry -- poor wording. I'll fix it. There is a `UICollectionViewDelegateFlowLayout` protocol that the collection view's delegate can adopt to support a flow layout. See the method [`– collectionView:layout:minimumInteritemSpacingForSectionAtIndex:`](https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewDelegateFlowLayout_protocol/Reference/Reference.html#//apple_ref/occ/intfm/UICollectionViewDelegateFlowLayout/collectionView:layout:minimumInteritemSpacingForSectionAtIndex:). – Caleb Jun 02 '14 at 06:27
4

A little bit of maths does the trick more easily. The code wrote by Chris Wagner is horrible because it calls the layout attributes of each previous items. So the more you scroll, the more it's slow...

Just use modulo like this (I'm using my minimumInteritemSpacing value as a max value too):

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
    NSInteger numberOfItemsPerLine = floor([self collectionViewContentSize].width / [self itemSize].width);

    if (indexPath.item % numberOfItemsPerLine != 0)
    {
        NSInteger cellIndexInLine = (indexPath.item % numberOfItemsPerLine);

        CGRect itemFrame = [currentItemAttributes frame];
        itemFrame.origin.x = ([self itemSize].width * cellIndexInLine) + ([self minimumInteritemSpacing] * cellIndexInLine);
        currentItemAttributes.frame = itemFrame;
    }

    return currentItemAttributes;
}
pkamb
  • 26,648
  • 20
  • 124
  • 157
ldesroziers
  • 226
  • 2
  • 4
  • 2
    In my situation the previous cell's size is important, which is why it calls for the layout attributes of the previous cell. I didn't performance test it on a large collection view. My use case has at most 11 cells so it was never an issue. – Chris Wagner Nov 08 '13 at 07:54
3

An easy way to left-justify is to modify layoutAttributesForElementsInRect: in your subclass of UICollectionViewFlowLayout:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
    CGRect prevFrame = CGRectMake(-FLT_MAX, -FLT_MAX, 0, 0);
    for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes)
    {
        //fix blur
        CGRect theFrame = CGRectIntegral(layoutAttributes.frame);

        //left justify
        if(prevFrame.origin.x > -FLT_MAX &&
           prevFrame.origin.y >= theFrame.origin.y &&
           prevFrame.origin.y <= theFrame.origin.y) //workaround for float == warning
        {
            theFrame.origin.x = prevFrame.origin.x +
                                prevFrame.size.width + 
                                EXACT_SPACE_BETWEEN_ITEMS;
        }
        prevFrame = theFrame;

        layoutAttributes.frame = theFrame;
    }
    return allLayoutAttributes;
}
xytor
  • 350
  • 3
  • 14
3

Clean Swift solution, from an history of evolution:

  1. there was matt answer
  2. there was Chris Wagner lone items fix
  3. there was mokagio sectionInset and minimumInteritemSpacing improvement
  4. there was fanpyi Swift version
  5. now here is a simplified and clean version of mine:
open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
    open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
    }

    open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
            collectionView != nil else {
            // should never happen
            return nil
        }

        // if the current frame, once stretched to the full row intersects the previous frame then they are on the same row
        if indexPath.item != 0,
            let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
            currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.origin.y, width: .infinity, height: previousFrame.size.height)) {
            // the next item on a line
            currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
        } else {
            // the first item on a line
            currentItemAttributes.frame.origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left
        }
        return currentItemAttributes
    }

    func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
        return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
    }

    func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
        return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
    }
}

Usage: the spacing between items is determined by delegate's collectionView (_:layout:minimumInteritemSpacingForSectionAt:).

I put it on github, https://github.com/Coeur/UICollectionViewLeftAlignedLayout, where I actually added a feature of supporting both scroll directions (horizontal and vertical).

Cœur
  • 32,421
  • 21
  • 173
  • 232
2

The swift version of Chris solution.

class PazLeftAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout {
    var maxCellSpacing = 14.0
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        if var attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? Array<UICollectionViewLayoutAttributes> {
            for attributes in attributesToReturn {
                if attributes.representedElementKind == nil {
                    let indexPath = attributes.indexPath
                    attributes.frame = self.layoutAttributesForItemAtIndexPath(indexPath).frame;
                }
            }
            return attributesToReturn;
        }
        return super.layoutAttributesForElementsInRect(rect)
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
        let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)

        if let collectionViewFlowLayout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
            let sectionInset = collectionViewFlowLayout.sectionInset
            if (indexPath.item == 0) { // first item of section
                var frame = currentItemAttributes.frame;
                frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
                currentItemAttributes.frame = frame;

                return currentItemAttributes;
            }

            let previousIndexPath = NSIndexPath(forItem:indexPath.item-1, inSection:indexPath.section)
            let previousFrame = self.layoutAttributesForItemAtIndexPath(previousIndexPath).frame;
            let previousFrameRightPoint = Double(previousFrame.origin.x) + Double(previousFrame.size.width) + self.maxCellSpacing

            let currentFrame = currentItemAttributes.frame
            var width : CGFloat = 0.0
            if let collectionViewWidth = self.collectionView?.frame.size.width {
                width = collectionViewWidth
            }
            let strecthedCurrentFrame = CGRectMake(0,
                currentFrame.origin.y,
                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
                var frame = currentItemAttributes.frame;
                frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
                currentItemAttributes.frame = frame;
                return currentItemAttributes;
            }

            var frame = currentItemAttributes.frame;
            frame.origin.x = CGFloat(previousFrameRightPoint)
            currentItemAttributes.frame = frame;
        }
        return currentItemAttributes;
    }
}

To use it do the following:

    override func viewDidLoad() {
    super.viewDidLoad()
    self.collectionView.collectionViewLayout = self.layout
}
var layout : PazLeftAlignedCollectionViewFlowLayout {
    var layout = PazLeftAlignedCollectionViewFlowLayout()
    layout.itemSize = CGSizeMake(220.0, 230.0)
    layout.minimumLineSpacing = 12.0
    return layout
}
davidethell
  • 10,959
  • 5
  • 37
  • 59
zirinisp
  • 8,851
  • 5
  • 29
  • 36
2

The "problem" with UICollectionViewFlowLayout is that it applies a justified align to the cells: The first cell in a row is left-aligned, the last cell in a row is right-aligned and all other cells in between are evenly distributed with an equal spacing that's greater than the minimumInteritemSpacing.

There are already many great answers to this post that solve this problem by subclassing UICollectionViewFlowLayout. As a result you get a layout that aligns the cells left. Another valid solution to distribute the cells with a constant spacing is to align the cells right.

AlignedCollectionViewFlowLayout

I've created a UICollectionViewFlowLayout subclass as well that follows a similar idea as suggested by matt and Chris Wagner that can either align the cells

⬅︎ left:

Left-aligned layout

or ➡︎ right:

Right-aligned layout

You can simply download it from here, add the layout file to your project and set AlignedCollectionViewFlowLayout as your collection view's layout class:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

How it works (for left-aligned cells):

 +---------+----------------------------------------------------------------+---------+
 |         |                                                                |         |
 |         |     +------------+                                             |         |
 |         |     |            |                                             |         |
 | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
 |  inset  |     |intersection|        |                     |   line rect  |  inset  |
 |         |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -|         |
 | (left)  |     |            |             current item                    | (right) |
 |         |     +------------+                                             |         |
 |         |     previous item                                              |         |
 +---------+----------------------------------------------------------------+---------+

The concept here is to check if the current cell with index i and the previous cell with the index i−1 occupy the same line.

  • If they don't 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).
  • If they do, 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.

For right-aligned cells...

... you do the same vice-versa, i.e. you check the next cell with the index i+1 instead.

Community
  • 1
  • 1
Mischa
  • 12,670
  • 7
  • 51
  • 94
2

You can do it in two ways.

Firstly, do some modification in layoutAttributesForItem,

get the current attributes's layout via the previous layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame.

layoutAttributesForItem(at:): This method provides on demand layout information to the collection view. You need to override it and return the layout attributes for the item at the requested indexPath.

Secondly, new some attributes via UICollectionViewLayoutAttributes(forCellWith: indexPath),layout them wherever you want.

And the math is a little larger, because it performs the heavy lifting of the layout.

layoutAttributesForElements(in:): In this method you need to return the layout attributes for all the items inside the given rectangle. You return the attributes to the collection view as an array of UICollectionViewLayoutAttributes.


You can check My repo

  • you can put items left aligned

  • you can put items right aligned

  • you can put items right aligned and items reversed

dengApro
  • 3,145
  • 1
  • 22
  • 33
1

A cleaner swift version for people interested, based on Chris Wagner's answer:

class AlignLeftFlowLayout: UICollectionViewFlowLayout {

    var maximumCellSpacing = CGFloat(9.0)

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]

        for attributes in attributesToReturn ?? [] {
            if attributes.representedElementKind == nil {
                attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame
            }
        }

        return attributesToReturn
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
        let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
        let sectionInset = (self.collectionView?.collectionViewLayout as UICollectionViewFlowLayout).sectionInset

        if indexPath.item == 0 {
            let f = curAttributes.frame
            curAttributes.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
            return curAttributes
        }

        let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
        let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath).frame
        let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing

        let curFrame = curAttributes.frame
        let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)

        if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
            curAttributes.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
        } else {
            curAttributes.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
        }

        return curAttributes
    }
}
Kevin R
  • 6,952
  • 4
  • 35
  • 43
  • `CGRectIntersectsRect` is intuitive and easy to borrow. @matt's `CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10;` is better . Same result , and do less. And compare Y position is leaner. – dengST30 Jun 20 '19 at 03:50
0

Here it is for NSCollectionViewFlowLayout

class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout {

    var maximumCellSpacing = CGFloat(2.0)

    override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {

        let attributesToReturn = super.layoutAttributesForElementsInRect(rect)

        for attributes in attributesToReturn ?? [] {
            if attributes.representedElementKind == nil {
                attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath!)!.frame
            }
        }
        return attributesToReturn
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {

        let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
        let sectionInset = (self.collectionView?.collectionViewLayout as! NSCollectionViewFlowLayout).sectionInset

        if indexPath.item == 0 {
            let f = curAttributes!.frame
            curAttributes!.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
            return curAttributes
        }

        let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
        let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath)!.frame
        let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing

        let curFrame = curAttributes!.frame
        let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)

        if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
            curAttributes!.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
        } else {
            curAttributes!.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
        }

        return curAttributes
    }
}
Chris
  • 2,737
  • 2
  • 25
  • 27
-1

a swift version base on mokagio:https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift

class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
        if let attributesToReturn = attributesToReturn {
            for attributes in attributesToReturn {
                if attributes.representedElementKind == nil {
                    let indexpath = attributes.indexPath
                    if let attr = layoutAttributesForItemAtIndexPath(indexpath) {
                        attributes.frame = attr.frame
                    }

                }
            }
        }
        return attributesToReturn
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        if  let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath){
            let sectionInset = self.evaluatedSectionInsetForItemAtIndex(indexPath.section)
            let  isFirstItemInSection = indexPath.item == 0;
            let  layoutWidth = CGRectGetWidth(self.collectionView!.frame) - sectionInset.left - sectionInset.right;

            if (isFirstItemInSection) {
                currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
                return currentItemAttributes
            }

            let  previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)

            let previousFrame = layoutAttributesForItemAtIndexPath(previousIndexPath)?.frame ?? CGRectZero
            let  previousFrameRightPoint = previousFrame.origin.x + previousFrame.width
            let currentFrame = currentItemAttributes.frame;
            let  strecthedCurrentFrame = CGRectMake(sectionInset.left,
                currentFrame.origin.y,
                layoutWidth,
                currentFrame.size.height)
            // if the current frame, once left aligned to the left and stretched to the full collection view
            // widht intersects the previous frame then they are on the same line
            let  isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)

            if (isFirstItemInRow) {
                // make sure the first item on a line is left aligned
                currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
                return currentItemAttributes
            }

            var frame = currentItemAttributes.frame;
            frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacingForSectionAtIndex(indexPath.section)
            currentItemAttributes.frame = frame;
            return currentItemAttributes;

        }
        return nil
    }
    func evaluatedMinimumInteritemSpacingForSectionAtIndex(sectionIndex:Int) -> CGFloat {
        if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
            if delegate.respondsToSelector("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:") {
                return delegate.collectionView!(self.collectionView!, layout: self, minimumInteritemSpacingForSectionAtIndex: sectionIndex)

            }
        }
        return self.minimumInteritemSpacing

    }
    func evaluatedSectionInsetForItemAtIndex(index: Int) ->UIEdgeInsets {
        if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
            if  delegate.respondsToSelector("collectionView:layout:insetForSectionAtIndex:") {
                return delegate.collectionView!(self.collectionView!, layout: self, insetForSectionAtIndex: index)
            }
        }
        return self.sectionInset
    }
}
Tobi Nary
  • 4,387
  • 4
  • 26
  • 48
范陆离
  • 71
  • 1
  • 3