41

I have a UICollectionView with random cells. Is there any method that allows me to center rows?

This is how it looks by default:

[ x x x x x x ]  
[ x x x x x x ]  
[ x x         ]  

Here is what the desired layout looks like:

[ x x x x x ]  
[ x x x x x ]  
[    x x    ]  
Cesare
  • 8,326
  • 14
  • 64
  • 116
deycall
  • 413
  • 1
  • 4
  • 5

9 Answers9

46

I had to do something like this, but needed all the cells in the one section. It was pretty simple to extend UICollectionViewFlowLayout to center cells. I made a pod:

https://github.com/keighl/KTCenterFlowLayout

enter image description here

Kyle Truscott
  • 1,417
  • 1
  • 12
  • 17
23

A bit of background first - a UICollectionView is combined with a UICollectionViewLayout which determines how the cells get placed in the view. This means a collection view is very flexible (you can create almost any layout with it), but also means modifying layouts can be a little confusing.

Creating an entirely new layout class is complex, so instead you want to try and modify the default layout (UICollectionViewFlowLayout) to get your center alignment. To make it even simpler, you probably want to avoid subclassing the flow layout itself.

Here's one approach (it may not be the best approach, but it's the first one I can think of) - split your cells into two sections, as follows:

[ x x x x x ] <-- Section 1 
[ x x x x x ] <-- Section 1 
[    x x    ] <-- Section 2 

This should be fairly straightforward, provided you know the width of your scroll view and the number of cells that can fit in each row.

Then, use the collectionView:layout:insetForSectionAtIndex: delegate method to set the margins for your second section so as it appears to be vertically centered. Once you've done this, you just need to ensure you recompute the appropriate section divisions/insets so both portrait and landscape orientations can be supported.

There is a somewhat similar question here - How to center align the cells of a UICollectionView? - that goes into more details about the inset methods, although it's not quite trying to do the same thing that you are.

Community
  • 1
  • 1
lxt
  • 30,690
  • 5
  • 76
  • 83
  • Well i don't know is it the best way to do it but it works. And that all i needed. Thank you. – deycall May 14 '13 at 14:01
  • I think it's probably the simplest, in terms of number of lines / not subclassing the layout. If you end up doing more layout work you might want to consider that approach. – lxt May 14 '13 at 14:03
  • You can also subclass `UICollectionViewFlowLayout` instead of `UICollectionViewLayout`. The layout is still flow based with a minor change and doing this does most of the heavy lifting for you you just customise the bit you want. – Fogmeister May 14 '13 at 14:06
  • 1
    If i have cells with different widths, this approach seems to make it even more complex. Do you have any suggestions on how i can achieve this, with cells having different widths. Thanks – rordulu Jun 17 '14 at 10:48
17

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. Center UICollectionViewCells with fixed size

The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:) and UICollectionViewFlowLayout's itemSize in order to center the cells of 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()

        title = "Center cells"

        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 }) {
            // Get the total width of the cells on the same row
            let cellsTotalWidth = attributes.reduce(CGFloat(0)) { (partialWidth, attribute) -> CGFloat in
                partialWidth + attribute.size.width
            }

            // Calculate the initial left inset
            let totalInset = collectionView!.safeAreaLayoutGuide.layoutFrame.width - cellsTotalWidth - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(attributes.count - 1)
            var leftInset = (totalInset / 2 * 10).rounded(.down) / 10 + 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


#2. Center autoresizing UICollectionViewCells

The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout's estimatedItemSize and UILabel's preferredMaxLayoutWidth in order to center the cells of 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()

        title = "Center cells"

        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 }) {
            // Get the total width of the cells on the same row
            let cellsTotalWidth = attributes.reduce(CGFloat(0)) { (partialWidth, attribute) -> CGFloat in
                partialWidth + attribute.size.width
            }

            // Calculate the initial left inset
            let totalInset = collectionView!.safeAreaLayoutGuide.layoutFrame.width - cellsTotalWidth - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(attributes.count - 1)
            var leftInset = (totalInset / 2 * 10).rounded(.down) / 10 + 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


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

In case anyone has a CollectionView that has 2 columns, and if number of items is odd, the last item should be center aligned. Then use this

DNLastItemCenteredLayout

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *attributes = [super layoutAttributesForElementsInRect:rect];

    for (UICollectionViewLayoutAttributes *attribute in attributes) {
        NSInteger itemCount = [self.collectionView.dataSource collectionView:self.collectionView
                                                      numberOfItemsInSection:attribute.indexPath.section];
        if (itemCount % 2 == 1 && attribute.indexPath.item == itemCount - 1) {
            CGRect originalFrame = attribute.frame;
            attribute.frame = CGRectMake(self.collectionView.bounds.size.width/2-originalFrame.size.width/2,
                                         originalFrame.origin.y,
                                         originalFrame.size.width,
                                         originalFrame.size.height);
        }
    }

    return attributes;
}
onmyway133
  • 38,911
  • 23
  • 231
  • 237
2

This can be achieved with a (relatively) simple custom layout, subclassed from UICollectionViewFlowLayout. Here's an example in Swift:

/**
 * A simple `UICollectionViewFlowLayout` subclass that would make sure the items are center-aligned in the collection view, when scrolling vertically.
 */
class UICollectionViewFlowCenterLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let suggestedAttributes = super.layoutAttributesForElementsInRect(rect) else { return nil }

        guard scrollDirection == .Vertical else { return suggestedAttributes }

        var newAttributes: [UICollectionViewLayoutAttributes] = []

        /// We will collect items for each row in this array
        var currentRowAttributes: [UICollectionViewLayoutAttributes] = []
        /// We will use this variable to detect new rows when iterating over items
        var yOffset:CGFloat = sectionInset.top
        for attributes in suggestedAttributes {
            /// If we happen to run into a new row...
            if attributes.frame.origin.y != yOffset {
                /*
                 * Update layout of all items in the previous row and add them to the resulting array
                 */
                centerSingleRowWithItemsAttributes(&currentRowAttributes, rect: rect)
                newAttributes += currentRowAttributes
                /*
                 * Reset the accumulated values for the new row
                 */
                currentRowAttributes = []
                yOffset = attributes.frame.origin.y
            }
            currentRowAttributes += [attributes]
        }
        /*
         * Update the layout of the last row.
         */
        centerSingleRowWithItemsAttributes(&currentRowAttributes, rect: rect)
        newAttributes += currentRowAttributes

        return newAttributes
    }

    /**
     Updates the attributes for items, so that they are center-aligned in the given rect.

     - parameter attributes: Attributes of the items
     - parameter rect:       Bounding rect
     */
    private func centerSingleRowWithItemsAttributes(inout attributes: [UICollectionViewLayoutAttributes], rect: CGRect) {
        guard let item = attributes.last else { return }

        let itemsCount = CGFloat(attributes.count)
        let sideInsets = rect.width - (item.frame.width * itemsCount) - (minimumInteritemSpacing * (itemsCount - 1))
        var leftOffset = sideInsets / 2

        for attribute in attributes {
            attribute.frame.origin.x = leftOffset
            leftOffset += attribute.frame.width + minimumInteritemSpacing
        }
    }
}
Alex Staravoitau
  • 1,972
  • 18
  • 23
2

I've subclassed UICollectionViewFlowLayout - altered the code i've found here for left aligned collection view.

  1. Left align the collection view
  2. Group the attributes for line arrays
  3. For each line:
    • calculate the space on the right side
    • add half of the the space for each attribute of the line

It's looks like this:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];

    CGFloat leftMargin = self.sectionInset.left;
    NSMutableArray *lines = [NSMutableArray array];
    NSMutableArray *currLine = [NSMutableArray array];

    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        // Handle new line
        BOOL newLine = attributes.frame.origin.x <= leftMargin;
        if (newLine) {
            leftMargin = self.sectionInset.left; //will add outside loop
            currLine = [NSMutableArray arrayWithObject:attributes];
        } else {
            [currLine addObject:attributes];
        }

        if ([lines indexOfObject:currLine] == NSNotFound) {
            [lines addObject:currLine];
        }

        // Align to the left
        CGRect newLeftAlignedFrame = attributes.frame;
        newLeftAlignedFrame.origin.x = leftMargin;
        attributes.frame = newLeftAlignedFrame;

        leftMargin += attributes.frame.size.width + self.minimumInteritemSpacing;
        [newAttributesForElementsInRect addObject:attributes];
    }

    // Center left aligned lines
    for (NSArray *line in lines) {
        UICollectionViewLayoutAttributes *lastAttributes = line.lastObject;
        CGFloat space = CGRectGetWidth(self.collectionView.frame) - CGRectGetMaxX(lastAttributes.frame);

        for (UICollectionViewLayoutAttributes *attributes in line) {
            CGRect newFrame = attributes.frame;
            newFrame.origin.x = newFrame.origin.x + space / 2;
            attributes.frame = newFrame;

        }
    }

    return newAttributesForElementsInRect;
}

Hope it helps someone :)

Community
  • 1
  • 1
chents
  • 383
  • 3
  • 13
1

Just link this flow layout. You can align center, left, right also.

 //
    //  CellAllignmentFlowLayout.swift
    //  UICollectionView
    //
    //  Created by rajeshkumar Lingavel on 8/11/15.
    //  Copyright © 2015 rajeshkumar Lingavel. All rights reserved.
    //

import UIKit
enum   SZAlignment:Int {
    case Center,
        left,
        Right
}
class CellAllignmentFlowLayout: UICollectionViewFlowLayout {
    var alignment:SZAlignment!
    var padding:CGFloat!
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
//        NSArray *allAttributesInRect = [super
//            layoutAttributesForElementsInRect:rect];

        let allAttributesInRect:NSArray = super.layoutAttributesForElementsInRect(rect)!
        var changedAttributes:NSArray = NSArray()
        switch(alignment.rawValue){
            case 0:
               changedAttributes  =   alignCenter(allAttributesInRect)
            case 1:
                changedAttributes =  alignLeft(allAttributesInRect)
            case 2:
                changedAttributes =  alignRight(allAttributesInRect)
            default:
                assertionFailure("No Direction")
        }

        return changedAttributes as? [UICollectionViewLayoutAttributes]
    }


   private func alignCenter(allAttributesInRect:NSArray) -> NSArray{


        let numberOfSection:Int = (self.collectionView?.numberOfSections())!

        let redefiendArray = NSMutableArray()

        for  i in 0 ..< numberOfSection {

                let thisSectionObjects = sectionObjects(allAttributesInRect, section: i)
                let totalLines = numberOfLines(thisSectionObjects, section: i)
                let lastrowObjects = lastRow(thisSectionObjects, numberOfRows: totalLines, section: i)
                let lastRowObjectsRow =  setMiddleTheLastRow(lastrowObjects)
                let start = (thisSectionObjects.count - lastrowObjects.count)

                for j in start..<thisSectionObjects.count{
                    thisSectionObjects.replaceObjectAtIndex(j, withObject: lastRowObjectsRow.objectAtIndex(j - start))
                }
            redefiendArray.addObjectsFromArray(thisSectionObjects as [AnyObject])
        }




        return redefiendArray
    }
   private func alignLeft(allAttributesInRect:NSArray) -> NSArray{


        return allAttributesInRect;

    }
   private func alignRight(allAttributesInRect:NSArray) -> NSArray{
        return allAttributesInRect;

    }

   private func getTotalLenthOftheSection(section:Int,allAttributesInRect:NSArray) -> CGFloat{

        var totalLength:CGFloat = 0.0
        totalLength = totalLength + (CGFloat (((self.collectionView?.numberOfItemsInSection(section))! - 1)) * padding)
        for  attributes in allAttributesInRect {

            if(attributes.indexPath.section == section){
                totalLength = totalLength + attributes.frame.width
            }
        }

        return totalLength
    }

   private func numberOfLines(allAttributesInRect:NSArray,section:Int)-> Int{
        var totalLines:Int = 0
        for  attributes in allAttributesInRect {
            if(attributes.indexPath.section == section){
                if (attributes.frame.origin.x == self.sectionInset.left){
                    totalLines = totalLines + 1
                }
            }
        }
        return totalLines
    }
   private func sectionObjects(allAttributesInRect:NSArray,section:Int) -> NSMutableArray{
        let objects:NSMutableArray = NSMutableArray()
        for  attributes in allAttributesInRect {
            if(attributes.indexPath.section == section){
                objects.addObject(attributes)
            }
        }
        return objects
    }

   private func lastRow(allAttributesInRect:NSArray,numberOfRows:Int,section:Int) -> NSMutableArray{
        var totalLines:Int = 0
        let lastRowArrays:NSMutableArray = NSMutableArray()
        for  attributes in allAttributesInRect {
            if(attributes.indexPath.section == section){
                if (attributes.frame.origin.x == self.sectionInset.left){
                    totalLines = totalLines + 1
                    if(totalLines == numberOfRows){
                        lastRowArrays.addObject(attributes)
                    }
                }
                else{
                    if(totalLines == numberOfRows){
                        lastRowArrays.addObject(attributes)
                    }
                }
            }
        }
        return lastRowArrays
    }
   private func setMiddleTheLastRow(lastRowAttrs:NSMutableArray)->NSMutableArray{
        let redefinedValues = NSMutableArray()
        let totalLengthOftheView = self.collectionView?.frame.width
        var totalLenthOftheCells:CGFloat = 0.0
        totalLenthOftheCells = totalLenthOftheCells + (CGFloat (lastRowAttrs.count) - 1) * padding

        for attrs in lastRowAttrs{
            totalLenthOftheCells = totalLenthOftheCells + attrs.frame.width
        }

        var initalValue = (totalLengthOftheView!/2) - (totalLenthOftheCells/2)

        for  i in 0..<lastRowAttrs.count {
            let changeingAttribute:UICollectionViewLayoutAttributes = lastRowAttrs[i] as! UICollectionViewLayoutAttributes
            var frame = changeingAttribute.frame
            frame.origin.x = initalValue
            changeingAttribute.frame = frame
            redefinedValues.addObject(changeingAttribute)
            initalValue = initalValue + changeingAttribute.frame.width + padding
        }

        return redefinedValues;
    }


}
Rajesh Kumar
  • 782
  • 9
  • 18
1

Swift 3.0 version of the class:

class UICollectionViewFlowCenterLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let suggestedAttributes = super.layoutAttributesForElements(in: rect) else { return nil }

        guard scrollDirection == .vertical else { return suggestedAttributes }

        var newAttributes: [UICollectionViewLayoutAttributes] = []

        var currentRowAttributes: [UICollectionViewLayoutAttributes] = []
        var yOffset:CGFloat = sectionInset.top
        for attributes in suggestedAttributes {
            if attributes.frame.origin.y != yOffset {
                centerSingleRowWithItemsAttributes(attributes: &currentRowAttributes, rect: rect)
                newAttributes += currentRowAttributes
                currentRowAttributes = []
                yOffset = attributes.frame.origin.y
            }
            currentRowAttributes += [attributes]
        }
        centerSingleRowWithItemsAttributes(attributes: &currentRowAttributes, rect: rect)
        newAttributes += currentRowAttributes

        return newAttributes
    }


    private func centerSingleRowWithItemsAttributes( attributes: inout [UICollectionViewLayoutAttributes], rect: CGRect) {
        guard let item = attributes.last else { return }

        let itemsCount = CGFloat(attributes.count)
        let sideInsets = rect.width - (item.frame.width * itemsCount) - (minimumInteritemSpacing * (itemsCount - 1))
        var leftOffset = sideInsets / 2

        for attribute in attributes {
            attribute.frame.origin.x = leftOffset
            leftOffset += attribute.frame.width + minimumInteritemSpacing
        }
    }
}
danielemm
  • 1,526
  • 1
  • 13
  • 22
1

I made Swift 4 version of Kyle Truscott answer:

import UIKit

class CenterFlowLayout: UICollectionViewFlowLayout {

    private var attrCache = [IndexPath: UICollectionViewLayoutAttributes]()

    override func prepare() {
        attrCache = [:]
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var updatedAttributes = [UICollectionViewLayoutAttributes]()

        let sections = self.collectionView?.numberOfSections ?? 0
        var section = 0
        while section < sections {
            let items = self.collectionView?.numberOfItems(inSection: section) ?? 0
            var item = 0
            while item < items {
                let indexPath = IndexPath(row: item, section: section)

                if let attributes = layoutAttributesForItem(at: indexPath), attributes.frame.intersects(rect) {
                    updatedAttributes.append(attributes)
                }

                let headerKind = UICollectionElementKindSectionHeader
                if let headerAttributes = layoutAttributesForSupplementaryView(ofKind: headerKind, at: indexPath) {
                    updatedAttributes.append(headerAttributes)
                }

                let footerKind = UICollectionElementKindSectionFooter
                if let footerAttributes = layoutAttributesForSupplementaryView(ofKind: footerKind, at: indexPath) {
                    updatedAttributes.append(footerAttributes)
                }

                item += 1
            }

            section += 1
        }

        return updatedAttributes
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        if let attributes = attrCache[indexPath] {
            return attributes
        }

        // Find the other items in the same "row"
        var rowBuddies = [UICollectionViewLayoutAttributes]()

        // Calculate the available width to center stuff within
        // sectionInset is NOT applicable here because a) we're centering stuff
        // and b) Flow layout has arranged the cells to respect the inset. We're
        // just hijacking the X position.
        var collectionViewWidth: CGFloat = 0
        if let collectionView = collectionView {
            collectionViewWidth = collectionView.bounds.width - collectionView.contentInset.left
                    - collectionView.contentInset.right
        }

        // To find other items in the "row", we need a rect to check intersects against.
        // Take the item attributes frame (from vanilla flow layout), and stretch it out
        var rowTestFrame: CGRect = super.layoutAttributesForItem(at: indexPath)?.frame ?? .zero
        rowTestFrame.origin.x = 0
        rowTestFrame.size.width = collectionViewWidth

        let totalRows = self.collectionView?.numberOfItems(inSection: indexPath.section) ?? 0

        // From this item, work backwards to find the first item in the row
        // Decrement the row index until a) we get to 0, b) we reach a previous row
        var rowStartIDX = indexPath.row
        while true {
            let prevIDX = rowStartIDX - 1

            if prevIDX < 0 {
                break
            }

            let prevPath = IndexPath(row: prevIDX, section: indexPath.section)
            let prevFrame: CGRect = super.layoutAttributesForItem(at: prevPath)?.frame ?? .zero

            // If the item intersects the test frame, it's in the same row
            if prevFrame.intersects(rowTestFrame) {
                rowStartIDX = prevIDX
            } else {
                // Found previous row, escape!
                break
            }
        }

        // Now, work back UP to find the last item in the row
        // For each item in the row, add it's attributes to rowBuddies
        var buddyIDX = rowStartIDX
        while true {
            if buddyIDX > totalRows - 1 {
                break
            }

            let buddyPath = IndexPath(row: buddyIDX, section: indexPath.section)

            if let buddyAttributes = super.layoutAttributesForItem(at: buddyPath),
               buddyAttributes.frame.intersects(rowTestFrame),
               let buddyAttributesCopy = buddyAttributes.copy() as? UICollectionViewLayoutAttributes {
                // If the item intersects the test frame, it's in the same row
                rowBuddies.append(buddyAttributesCopy)
                buddyIDX += 1
            } else {
                // Encountered next row
                break
            }
        }

        let flowDelegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout
        let selector = #selector(UICollectionViewDelegateFlowLayout.collectionView(_:layout:minimumInteritemSpacingForSectionAt:))
        let delegateSupportsInteritemSpacing = flowDelegate?.responds(to: selector) ?? false

        // x-x-x-x ... sum up the interim space
        var interitemSpacing = minimumInteritemSpacing

        // Check for minimumInteritemSpacingForSectionAtIndex support
        if let collectionView = collectionView, delegateSupportsInteritemSpacing && rowBuddies.count > 0 {
            interitemSpacing = flowDelegate?.collectionView?(collectionView,
                    layout: self,
                    minimumInteritemSpacingForSectionAt: indexPath.section) ?? 0
        }

        let aggregateInteritemSpacing = interitemSpacing * CGFloat(rowBuddies.count - 1)

        // Sum the width of all elements in the row
        var aggregateItemWidths: CGFloat = 0
        for itemAttributes in rowBuddies {
            aggregateItemWidths += itemAttributes.frame.width
        }

        // Build an alignment rect
        // |  |x-x-x-x|  |
        let alignmentWidth = aggregateItemWidths + aggregateInteritemSpacing
        let alignmentXOffset: CGFloat = (collectionViewWidth - alignmentWidth) / 2

        // Adjust each item's position to be centered
        var previousFrame: CGRect = .zero
        for itemAttributes in rowBuddies {
            var itemFrame = itemAttributes.frame

            if previousFrame.equalTo(.zero) {
                itemFrame.origin.x = alignmentXOffset
            } else {
                itemFrame.origin.x = previousFrame.maxX + interitemSpacing
            }

            itemAttributes.frame = itemFrame
            previousFrame = itemFrame

            // Finally, add it to the cache
            attrCache[itemAttributes.indexPath] = itemAttributes
        }

        return attrCache[indexPath]
    }
}
Nominalista
  • 3,669
  • 7
  • 32
  • 80