NSCollectionViewFlowLayout - left alignment
Asked Answered
G

4

5

NSCollectionViewFlowLayout produces a layout with items justified on the right margin or, if the container is only wide enough for one item, centres items. I was expecting an alignment option, e.g. on the delegate, but am not finding anything in the docs. Does it require subclassing NSCollectionViewFlowLayout to achieve this?

Giraffe answered 29/3, 2016 at 13:27 Comment(0)
G
12

Here is a subclass that produces a left justified flow layout:

class LeftFlowLayout: NSCollectionViewFlowLayout {

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

        let defaultAttributes = super.layoutAttributesForElementsInRect(rect)

        if defaultAttributes.isEmpty {
            // we rely on 0th element being present,
            // bail if missing (when there's no work to do anyway)
            return defaultAttributes
        }

        var leftAlignedAttributes = [NSCollectionViewLayoutAttributes]()

        var xCursor = self.sectionInset.left // left margin

        // if/when there is a new row, we want to start at left margin
        // the default FlowLayout will sometimes centre items,
        // i.e. new rows do not always start at the left edge

        var lastYPosition = defaultAttributes[0].frame.origin.y

        for attributes in defaultAttributes {
            if attributes.frame.origin.y > lastYPosition {
                // we have changed line
                xCursor = self.sectionInset.left
                lastYPosition = attributes.frame.origin.y
            }

            attributes.frame.origin.x = xCursor
            // by using the minimumInterimitemSpacing we no we'll never go
            // beyond the right margin, so no further checks are required
            xCursor += attributes.frame.size.width + minimumInteritemSpacing

            leftAlignedAttributes.append(attributes)
        }
        return leftAlignedAttributes
    }
}
Giraffe answered 29/3, 2016 at 13:27 Comment(2)
There is a bug here: if attributes.frame.origin.y != lastYPosition should be if attributes.frame.origin.y > lastYPosition to prevent a scenario where the new position is less than the last position. This causes UI elements to be drawn overlapping one another. (my edit was rejected - posting this fix as a comment instead)Wanhsien
That fix does not work when the next item has a height that is less than the preceding item. If Item1 starts at 0 and is 200.0 tall but Item2 is 100.0 tall, then Item2's Y-origin will be 50.0 (by default it's centered in the row). That will meet the ">lastYPosition" check and Item2's X-origin will be incorrectly set. The answer is to ask: "Is this item's Y-origin greater than the last Item's Y-origin PLUS the last item's height?" If yes, then we have started a new row.Ranket
R
5

@Obliquely's answer fails when the collectionViewItems are not uniform in height. Here is their code modified to handle non-uniformly-sized items in Swift 4.2:

class CollectionViewLeftFlowLayout: NSCollectionViewFlowLayout
{
    override func layoutAttributesForElements(in rect: CGRect) -> [NSCollectionViewLayoutAttributes]
    {
        let defaultAttributes = super.layoutAttributesForElements(in: rect)

        if defaultAttributes.isEmpty {
            return defaultAttributes
        }

        var leftAlignedAttributes = [NSCollectionViewLayoutAttributes]()

        var xCursor = self.sectionInset.left                            // left margin
        var lastYPosition = defaultAttributes[0].frame.origin.y         // if/when there is a new row, we want to start at left margin
        var lastItemHeight = defaultAttributes[0].frame.size.height

        for attributes in defaultAttributes
        {
            // copy() Needed to avoid warning from CollectionView that cached values are mismatched
            guard let newAttributes = attributes.copy() as? NSCollectionViewLayoutAttributes else {
                continue;
            }

            if newAttributes.frame.origin.y > (lastYPosition + lastItemHeight)
            {
                // We have started a new row
                xCursor = self.sectionInset.left
                lastYPosition = newAttributes.frame.origin.y
            }

            newAttributes.frame.origin.x = xCursor

            xCursor += newAttributes.frame.size.width + minimumInteritemSpacing
            lastItemHeight = newAttributes.frame.size.height

            leftAlignedAttributes.append(newAttributes)
        }
        return leftAlignedAttributes
    }
}
Ranket answered 30/10, 2018 at 6:20 Comment(0)
C
5

A shorter solution for swift 4.2:

class CollectionViewLeftFlowLayout: NSCollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
        let attributes = super.layoutAttributesForElements(in: rect)

        if attributes.isEmpty { return attributes }

        var leftMargin = sectionInset.left
        var lastYPosition = attributes[0].frame.maxY

        for itemAttributes in attributes {

            if itemAttributes.frame.origin.y > lastYPosition { // NewLine
               leftMargin = sectionInset.left
            }

            itemAttributes.frame.origin.x = leftMargin
            leftMargin += itemAttributes.frame.width + minimumInteritemSpacing
            lastYPosition = itemAttributes.frame.maxY
        }
        return attributes
    }
}
Corrupt answered 23/3, 2019 at 17:47 Comment(0)
H
0

In case your items have the same width...

In the other delegate method, you should change the frame of the NSCollectionViewLayoutAttributes

- (NSCollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSCollectionViewLayoutAttributes *attributes = [[super layoutAttributesForItemAtIndexPath:indexPath] copy];
    NSRect                           modifiedFrame = [attributes frame];

    modifiedFrame.origin.x = floor(modifiedFrame.origin.x / (modifiedFrame.size.width + [self minimumInteritemSpacing])) * (modifiedFrame.size.width + [self minimumInteritemSpacing]);

    [attributes setFrame:modifiedFrame];

    return [attributes autorelease];
}
Hotbed answered 14/3, 2019 at 15:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.