Self Sizing UICollectionViewCell in Swift

The iPhone and iOS has been around for more than a decade. UIKit has been around for that long as well. The iPhone started as one form factor one version of the iOS. Screen sizes have exploded over the preceding 10 years.

UITableView was the original way to lay out data in a list or tabular form. UITableViewCells were generally fixed and size and mostly static in content.

UICollectionView was introduced in iOS 6. It is a newer, cleaner API that can adapt to multiple screen sizes on the iPhone and have a grid layout on iPad. UICollectionView is more featureful than UITableView and as a result can be more confusing to configure.

UICollectionViewDataSource may look familiar, but UICollectionViewFlowLayoutDelegate and UICollectionViewLayoutAttributes can be confusing. Extra configuration is required if you want to have a UICollectionViewCell take the full width of the screen on any iPhone screen just like a UITableViewCell.

Creating a UICollectionViewController

You can follow along with example code in the SelfSizingUICollectionViewCell repository.

We start with the simplest, single window app that we can create that loads a UICollectionViewController from Main.storyboard.

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
}

Create a UICollectionViewController in the storyboard and make sure that Is Initial View Controller is toggled on.

Self Sizing UICollectionViewController Storyboard

We will now conform to the UICollectionViewDataSource to display a SelfSizingCollectionViewCell.

class SelfSizingViewController: UICollectionViewController {

    // MARK: - UICollectionViewDataSource
    
    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 1
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SelfSizingCollectionViewCell", for: indexPath) as? SelfSizingCollectionViewCell else {
            return UICollectionViewCell()
        }
        
        cell.label1.text = "This is label one.\nThis is label one.\nThis is label one.\nThis is label one.\nThis is label one."
        cell.label2.text = "This is label two.\nThis is label two.\nThis is label two.\nThis is label two.\nThis is label two."
        
        return cell
    }
}

Configuring SelfSizingViewController on viewDidLoad

The first step in configuring Auto Layout to create Self Sizing UICollectionViewCells is to configure UICollectionViewFlowLayout. This is done in the viewDidLoad method in SelfSizingViewController.

class SelfSizingViewController: UICollectionViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout,
           let collectionView = collectionView {
            let w = collectionView.frame.width - 20
            flowLayout.estimatedItemSize = CGSize(width: w, height: 200)
        }
    }
}

The above code calculates the width of a cell by taking the width of the entire collection view and subtracting 20 points for the margins.

Implementing SelfSizingCollectionViewCell

We create a SelfSizingCollectionViewCell in the storyboard that has two UILabels as outlets.

The two labels are constrained to the edges of SelfSizingCollectionViewCell. And a vertical constraint is added between the top and bottom label. For the cell to self size based upon the contents of the labels, the second label needs a Vertical Content Hugging Priority of 250.

Self Sizing UICollectionViewCell Storyboard

preferredLayoutAttributesFitting

And finally in SelfSizingCollectionViewCell, we must implement preferredLayoutAttributesFitting

class SelfSizingCollectionViewCell: UICollectionViewCell {
    
    @IBOutlet var label1: UILabel!
    @IBOutlet var label2: UILabel!
    
    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        setNeedsLayout()
        layoutIfNeeded()
        let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
        var frame = layoutAttributes.frame
        frame.size.height = ceil(size.height)
        layoutAttributes.frame = frame
        return layoutAttributes
    }
}