8

So, I'm trying to implement a tag list with UICollectionView. I'm following this tutorial: http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/

The issue is flow layout in UICollectionView tries to space items on the same row evenly.

enter image description here

As a developer, I can only specify minimumInteritemSpacingForSectionAtIndex, it's really up to the UICollectionView to determine the actual item spacing.

But what I really want to achieve is like this:

enter image description here

Any ideas?

Chris Chen
  • 5,307
  • 4
  • 45
  • 49

2 Answers2

12

I've converted Milo's solution to Swift: https://github.com/Coeur/UICollectionViewLeftAlignedLayout/

It simply subclasses UICollectionViewFlowLayout.

import UIKit

/**
 *  Simple UICollectionViewFlowLayout that aligns the cells to the left rather than justify them
 *
 *  Based on https://stackoverflow.com/questions/13017257/how-do-you-determine-spacing-between-cells-in-uicollectionview-flowlayout
 */
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,
            let collectionView = self.collectionView else {
            // should never happen
            return nil
        }

        let sectionInset = evaluatedSectionInsetForSection(at: indexPath.section)

        guard indexPath.item != 0 else {
            currentItemAttributes.leftAlignFrame(withSectionInset: sectionInset)
            return currentItemAttributes
        }

        guard let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame else {
            // should never happen
            return nil
        }

        // if the current frame, once left aligned to the left and stretched to the full collection view
        // width intersects the previous frame then they are on the same line
        guard previousFrame.intersects(CGRect(x: sectionInset.left, y: currentItemAttributes.frame.origin.y, width: collectionView.frame.width - sectionInset.left - sectionInset.right, height: currentItemAttributes.frame.size.height)) else {
            // make sure the first item on a line is left aligned
            currentItemAttributes.leftAlignFrame(withSectionInset: sectionInset)
            return currentItemAttributes
        }

        currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
        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
    }
}

extension UICollectionViewLayoutAttributes {
    func leftAlignFrame(withSectionInset sectionInset: UIEdgeInsets) {
        frame.origin.x = sectionInset.left
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
  • 1
    This should be marked as the right answer – thedp Mar 04 '19 at 13:59
  • @thedp Question being marked as duplicate, there is no need for an accepted answer. And I've posted almost the same answer in the other question. – Cœur Mar 04 '19 at 14:06
1

Apple has provided the UICollectionViewFlowLayout class for us developers, which should be enough to solve the 'typical use case' of collection views. However, I believe you're correct in your assessment that the default layout does not allow you to create this tag cloud effect. If you need something different from the normal flow layout, you'll have to write your own subclass of UICollectionViewLayout.

Apple covers this topic in their 2012 WWDC session titled, "Advanced Collection Views and Building Custom Layouts"

Some additional Apple docs: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html

At the risk of seeming biased, I also wrote a quick blog post running through the basic steps: http://bradbambara.wordpress.com/2014/05/24/getting-started-with-custom-uicollectionview-layouts/

Hope that helps.

BradB
  • 509
  • 5
  • 15