Auto Sizing Collection Views

Interface Builder

We’ll be building the auto-sizing collection view in Interface Builder. Start with a basic UICollectionViewController and adjust the size of the cell to roughly what a typical cell size would be.

Start

In order for Auto Layout to figure out the cell size, there needs to be a view inside the cell with constaints to at least the trailing and bottom spaces of the cell. So add a basic UIView inside of the cell and set its constaints to hug the border of the cell.

Start

Our cell will be simple for the purposes of this tutorial. Add two labels to the view inside the cell. To provide accessibily support , we can easily set these labels to use dynamic text sizing by setting their fonts to use text styles. I selected the Header 1 and Body text styles, but any can be used. Make sure to check the Automatically Adjusts Font box as well!

Start

Since the body text has an unknown size, it’s also a good idea to set its number of lines to 0.

Now, add constaints to the labels.

Start

Code

The code to make a collection view auto-sizing is super simple. To make it even simpler, we can stuff it into a protocol with a default implementation and just tell Swift our UIViewController subclass conforms to it.

protocol AutoSizingCollectionView: class {
    weak var collectionView: UICollectionView? { get set }
    
    /**
     Configures the collection view to use automatic size
     */
    func setAutoSizing()
    
    /**
     Reloads the collection view's data and layout -- necessary for custom drawing in cells
     such as shadows and rounded corners
     */
    func reloadAndLayout()
}

Because most UICollectionViewController subclasses we write will not need any custom configuration, we can greatly reduce code reuse by adding a default implementation for this protocol using an extension.

extension AutoSizingCollectionView {
    func setAutoSizing() {
        if let layout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
            layout.estimatedItemSize = CGSize(width: 1, height: 1) // can be any size as long as it is smaller than cell
            layout.itemSize = UICollectionViewFlowLayoutAutomaticSize // tell the layout to use AutoLayout to size the cell
        }
    }
    func reloadAndLayout() {
        DispatchQueue.main.async { [weak self] in
            self?.collectionView?.reloadData()
            self?.collectionView?.setNeedsLayout()
            self?.collectionView?.layoutIfNeeded()
            self?.collectionView?.reloadData()
        }
    }
}

We also need a protocol to set the UICollectionViewCell subclass to be autosizing. Its implementation is just as simple:

protocol DynamicHeightCollectionViewCell: class {
    weak var widthConstraint: NSLayoutConstraint! { get set }
    var contentView: UIView { get }
    func setAutoSizing()
}

extension DynamicHeightCollectionViewCell {
    func setAutoSizing() {
        contentView.translatesAutoresizingMaskIntoConstraints = false
        let screenWidth = UIScreen.main.bounds.width
        let maxWidth = screenWidth - (2*16)
        widthConstraint.constant = maxWidth
    }
}

Notice that the protocol is called DynamicHeightCollectionViewCell. For the purposes of this app, we don’t want each cell to have a dynamic width AND height; rather, we want each cell to be as wide as the screen and have a dynamic height based off the length of the text in the cell. To do this, we require that each cell has a NSLayoutConstraint called widthConstraint.

Back in Interface Builder, let’s add this constraint. The constant it’s set to in the storyboard doesn’t matter – it will always be set in code later.

Adding width constraint

Now we’re ready to create our UICollectionViewController and UICollectionViewCell subclasses.

UICollectionViewController, by default, has a UICollectionView field named collectionView. So to conform to our AutoSizingCollectionView protocol, just add it to the class declaration:

class CollectionViewController: UICollectionViewController, AutoSizingCollectionView { /* ... */ }

To configure collectionView, we can use the default implementations in AutoSizingCollectionView. Add this in viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    setAutoSizing()
    reloadAndLayout()
}

To configure the CollectionViewCell, set your subclass to conform to DynamicHeightCollectionViewCell. Make IBOutlets for the constant width constraint and the text labels, and call setAutoSizing in awakeFromNib.

class AutoSizingCollectionViewCell: UICollectionViewCell, DynamicHeightCollectionViewCell {
    
    @IBOutlet weak var widthConstraint: NSLayoutConstraint!
    
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var textLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        setAutoSizing()
    }
}

Now, back in our CollectionViewController, we can implement the UICollectionViewDataSource methods. To test the auto-sizing, just use some mock data such as:

let data = [
        """
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        """,
        """
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        """,
        """
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        """,
        """
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        Test string Test string Test string Test string Test string Test string
        """,
        
        ]

The UICollectionViewDataSource methods are just straight forward implementations:

// MARK: UICollectionViewDataSource

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }


    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? AutoSizingCollectionViewCell else { fatalError("cast failed") }
        
        cell.titleLabel.text = "Cell \(indexPath.item)"
        cell.textLabel.text = data[indexPath.item]
    
        return cell
    }

Ensure you’ve set everything in the storyboard to use the custom classes, and run the app.

Success

It works! As you can see, each cell has the same width and a dynamic height based on the size of the text labels. And, since we used text styles instead of statically sized fonts, the app will still look great when accessibilty settings are turned on.

Even better

I hope this post helped you! The source code for this project can be viewed here. Please feel free to contact me below or on Twitter @thezacwood if you have any questions!

comments powered by Disqus