UICollectionView Custom Layout Tutorial: Pinterest

UICollectionView Custom Layout Tutorial: Pinterest
create custom layouts

Create awesome user interfaces with collection views and custom layouts!

UICollectionView, introduced in iOS 6, has become one of the most popular UI elements among iOS developers. What makes it so attractive is the separation between the data and presentation layers, which depends upon a separate object to handle the layout. The layout is then responsible for determining the placement and visual attributes of the views.

You’ve likely used the default flow layout — a layout class provided by UIKit — which consists of a basic grid layout with some customizations. But you can also implement your own custom layouts to arrange the views any way you like; this is what makes the collection view so flexible and powerful.

In this tutorial, you’ll create a UICollectionView custom layout inspired by the popular Pinterest app.

In the process, you’ll learn a lot about custom layouts, how to calculate and cache layout attributes, how to handle dynamically sized cells and much more.

Note: This tutorial requires a basic knowledge of UICollectionView. If you’re not familiar with it, you can learn more about it in our written or video tutorial series:

Ready to pimp-up your collection view? Read on!

Getting Started

Download the starter project for this tutorial and open it in Xcode.

Build and run the project, and you’ll see the following:

Starter Project image

The app presents a gallery of pictures from RWDevCon. You can browse the photos and see how much fun the attendees had while at the conference.

The gallery is built using a collection view with a standard flow layout. At first sight, it looks all right. But the layout design could certainly be improved; the photos don’t completely fill the content area and long annotations end up truncated.

Creating Custom Collection View Layouts

Your first step in creating a stunning collection view is to create a custom layout class for your gallery.

Collection view layouts are subclasses of the abstract UICollectionViewLayout class; they define the visual attributes of every item in your collection view. The individual attributes are instances of UICollectionViewLayoutAttributes and contain the properties of each item in your collection view, such as the item’s frame or opacity.

Create a new file inside the Layouts group. Select Cocoa Touch Class from the iOS\Source list. Name it PinterestLayout and make it a subclass of UICollectionViewLayout. Make sure the selected language is Swift and finally create the file.

Next you’ll need to configure the collection view to use your new layout.

Open Main.storyboard and select the Collection View in the Photo Stream View Controller Scene as shown below:


Next, open the Attributes Inspector. Select Custom in the Layout drop-down list and select PinterestLayout in the Class drop-down list:


Okay — time to see how it looks. Build and run your app:


collectionview empty meme

Don’t panic! This is a good sign, believe it or not. This means the collection view is using your custom layout class. The cells aren’t shown because the PinterestLayout class doesn’t yet implement any of the methods involved in the layout process.

Core Layout Process

Take a moment to think about the collection view layout process, which is a collaboration between the collection view and the layout object. When the collection view needs some layout information, it asks your layout object to provide it by calling certain methods in a specific order:

Layout lifecycle

Your layout subclass must implement the following methods:

  • prepareLayout(): This method is called whenever a layout operation is about to take place. It’s your opportunity to prepare and perform any calculations required to determine the collection view size and the positions of the items.
  • collectionViewContentSize(): In this method, you have to return the height and width of the entire collection view content — not just the visible content. The collection view uses this information internally to configure its scroll view content size.
  • layoutAttributesForElementsInRect(_:): In this method you need to return the layout attributes for all the items inside the given rectangle. You return the attributes to the collection view as an array of UICollectionViewLayoutAttributes.

Okay, so you know what you need to implement — but how do you go about calculating these attributes?

Calculating Layout Attributes

For this layout, you need to dynamically calculate the position and height of every item since you don’t know what the height of the photo or the annotation will be in advance. You’ll declare a protocol that will provide this position and height info when PinterestLayout needs it.

Now, back to the code. Open PinterestLayout.swift and add the following delegate protocol declaration before the PinterestLayout class:

protocol PinterestLayoutDelegate {
  // 1
  func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath, 
      withWidth:CGFloat) -> CGFloat
  // 2
  func collectionView(collectionView: UICollectionView, 
      heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat

This code declares the PinterestLayoutDelegate protocol, which has two methods to request the height of the photo (1) as well as the annotation (2). You’ll implement this protocol in PhotoStreamViewController shortly.

There’s just one more thing to do before implementing the layout methods; you need to declare some properties that will help with the layout process.

Add the following to PinterestLayout:

// 1 
var delegate: PinterestLayoutDelegate!
// 2
var numberOfColumns = 2
var cellPadding: CGFloat = 6.0
// 3
private var cache = [UICollectionViewLayoutAttributes]()
// 4
private var contentHeight: CGFloat  = 0.0
private var contentWidth: CGFloat {
let insets = collectionView!.contentInset
  return CGRectGetWidth(collectionView!.bounds) - (insets.left + insets.right)

This code defines some properties you’ll need later on to provide the layout information. Here it is, explained step-by-step:

  1. This keeps a reference to the delegate.
  2. These are two public properties for configuring the layout: the number of columns and the cell padding.
  3. This is an array to cache the calculated attributes. When you call prepareLayout(), you’ll calculate the attributes for all items and add them to the cache. When the collection view later requests the layout attributes, you can be efficient and query the cache instead of recalculating them every time.
  4. This declares two properties to store the content size. contentHeight is incremented as photos are added, and contentWidth is calculated based on the collection view width and its content inset.

You’re ready to calculate the attributes for the collection view items, which for now will consist of the frame. To understand how this will be done, take a look at the following diagram:


You’ll calculate the frame of every item based on its column (tracked by xOffset) and the position of the previous item in the same column (tracked by yOffset).

To calculate the horizontal position, you’ll use the starting X coordinate of the column the item belongs to, and then add the cell padding. The vertical position is the starting position of the prior item in that column, plus the height of that prior item. The overall item height is the sum of the image height, the annotation height and the content padding.

You’ll do this in prepareLayout(), where your primary objective is to calculate an instance of UICollectionViewLayoutAttributes for every item in the layout.

Add the following method to PinterestLayout:

override func prepareLayout() {
  // 1
  if cache.isEmpty {
    // 2
    let columnWidth = contentWidth / CGFloat(numberOfColumns)
    var xOffset = [CGFloat]()
    for column in 0 ..< numberOfColumns {
      xOffset.append(CGFloat(column) * columnWidth )
    var column = 0
    var yOffset = [CGFloat](count: numberOfColumns, repeatedValue: 0)
    // 3
    for item in 0 ..< collectionView!.numberOfItemsInSection(0) {
      let indexPath = NSIndexPath(forItem: item, inSection: 0)
      // 4
      let width = columnWidth - cellPadding * 2
      let photoHeight = delegate.collectionView(collectionView!, heightForPhotoAtIndexPath: indexPath, 
      let annotationHeight = delegate.collectionView(collectionView!,
          heightForAnnotationAtIndexPath: indexPath, withWidth: width)
      let height = cellPadding +  photoHeight + annotationHeight + cellPadding
      let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
      let insetFrame = CGRectInset(frame, cellPadding, cellPadding)
      // 5
      let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
      attributes.frame = insetFrame
      // 6
      contentHeight = max(contentHeight, CGRectGetMaxY(frame))
      yOffset[column] = yOffset[column] + height
      column = column >= (numberOfColumns - 1) ? 0 : ++column

Taking each numbered comment in turn:

  1. You only calculate the layout attributes if cache is empty.
  2. This declares and fills the xOffset array with the x-coordinate for every column based on the column widths. The yOffset array tracks the y-position for every column. You initialize each value in yOffset to 0, since this is the offset of the first item in each column.
  3. This loops through all the items in the first section, as this particular layout has only one section.
  4. This is where you perform the frame calculation. width is the previously calculated cellWidth, with the padding between cells removed. You ask the delegate for the height of the image and the annotation, and calculate the frame height based on those heights and the predefined cellPadding for the top and bottom. You then combine this with the x and y offsets of the current column to create the insetFrame used by the attribute.
  5. This creates an instance of UICollectionViewLayoutAttribute, sets its frame using insetFrame and appends the attributes to cache.
  6. This expands contentHeight to account for the frame of the newly calculated item. It then advances the yOffset for the current column based on the frame. Finally, it advances the column so that the next item will be placed in the next column.

Note: As prepareLayout() is called whenever the collection view’s layout is invalidated, there are many situations in a typical implementation where you might need to recalculate attributes here. For example, the bounds of the UICollectionView might change – such as when the orientation changes – or items may be added or removed from the collection. These cases are out of scope for this tutorial, but it’s important to be aware of them in a non-trivial implementation.

Next, add the following method to PinterestLayout:

override func collectionViewContentSize() -> CGSize {
  return CGSize(width: contentWidth, height: contentHeight)

This overrides collectionViewContentSize() of the abstract parent class, and returns the size of the collection view’s contents. To do this, you use both contentWidth and contentHeight calculated in the previous steps.

The last method you need to override is layoutAttributesForElementsInRect(_:), which the collection view calls after prepareLayout() to determine which items are visible in the given rect.

Add the following code to the very end ofPinterestLayout:

override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
  var layoutAttributes = [UICollectionViewLayoutAttributes]()
  for attributes  in cache {
    if CGRectIntersectsRect(attributes.frame, rect) {
  return layoutAttributes

Here you iterate through the attributes in cache and check if their frames intersect with rect, which is provided by the collection view. You add any attributes with frames that intersect with that rect to layoutAttributes, which is eventually returned to the collection view.

Before you can see your layout in action, you need to implement the layout delegate. PinterestLayout relies upon this to provide photo and annotation heights when calculating the height of an attribute’s frame.

Open PhotoStreamViewController.swift and add the following extension to the end of the file to implement the PinterestLayoutDelegate protocol:

extension PhotoStreamViewController : PinterestLayoutDelegate {
  // 1
  func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath,
      withWidth width:CGFloat) -> CGFloat {
    let photo = photos[indexPath.item]
    let boundingRect =  CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
    let rect  = AVMakeRectWithAspectRatioInsideRect(photo.image.size, boundingRect)
    return rect.size.height
  // 2
  func collectionView(collectionView: UICollectionView, 
      heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
    let annotationPadding = CGFloat(4)
    let annotationHeaderHeight = CGFloat(17)
    let photo = photos[indexPath.item]
    let font = UIFont(name: "AvenirNext-Regular", size: 10)!
    let commentHeight = photo.heightForComment(font, width: width)
    let height = annotationPadding + annotationHeaderHeight + commentHeight + annotationPadding
    return height

Here’s what’s going on in the code above:

  1. This provides the height of the photos. It uses AVMakeRectWithAspectRatioInsideRect() from AVFoundation to calculate a height that retains the photo’s aspect ratio, restricted to the cell’s width.
  2. This calls heightForComment(_:width:), a helper method included in the starter project that calculates the height of the photo’s comment based on the given font and the cell’s width. You then add that height to a hard-coded annotationPadding value for the top and bottom, as well as a hard-coded annotationHeaderHeight that accounts for the size of the annotation title.

Next, add the following code inside viewDidLoad(), just below the call to super:

if let layout = collectionView?.collectionViewLayout as? PinterestLayout {
  layout.delegate = self

This sets the PhotoStreamViewController as the delegate for your layout.

Time to see how things are shaping up! Build and run your app. You’ll see the cells are properly positioned and sized based on the heights of the photos and the annotations:


You’re getting there, but the image view isn’t filling all the available space. You’ll fix that using custom layout attributes.

Custom Layout Attributes

You now need to resize the cell’s image view to match the calculated height of the photo. To do that, you need to create a subclass of UICollectionViewLayoutAttributes.

By subclassing UICollectionViewLayoutAttributes, you can add your own properties, which are automatically passed to the cell. You can use these attributes by overriding applyLayoutAttributes(_:) in your UICollectionViewCell subclass, which your collection view calls during the layout process, as shown in the illustration below:


Open PinterestLayout.swift and add the following code add the top of the file, above the class declaration for PinterestLayout:

class PinterestLayoutAttributes: UICollectionViewLayoutAttributes {
  // 1
  var photoHeight: CGFloat = 0.0
  // 2
  override func copyWithZone(zone: NSZone) -> AnyObject {
    let copy = super.copyWithZone(zone) as! PinterestLayoutAttributes
    copy.photoHeight = photoHeight
    return copy
  // 3
  override func isEqual(object: AnyObject?) -> Bool {
    if let attributes = object as? PinterestLayoutAttributes {
      if( attributes.photoHeight == photoHeight  ) {
        return super.isEqual(object)
    return false

This declares a UICollectionViewLayoutAttributes subclass named PinterestLayoutAttributes. Here’s how it works, step-by-step:

  1. This declares the photoHeight property that the cell will use to resize its image view.
  2. This overrides copyWithZone(). Subclasses of UICollectionViewLayoutAttributes need to conform to the NSCopying protocol because the attribute’s objects can be copied internally. You override this method to guarantee that the photoHeight property is set when the object is copied.
  3. This overrides isEqual(_:), and it’s mandatory as well. The collection view determines whether the attributes have changed by comparing the old and new attribute objects using isEqual(_:). You must implement it to compare the custom properties of your subclass. The code compares the photoHeight of both instances, and if they are equal, calls super to determine if the inherited attributes are the same. If the photo heights are different, it returns false.

Next, add the following method to PinterestLayout :

override class func layoutAttributesClass() -> AnyClass {
  return PinterestLayoutAttributes.self

This overrides layoutAttributesClass() to tell the collection view to use PinterestLayoutAttributes whenever it creates layout attributes objects.

Next, you need to change the references to UICollectionViewLayoutAttributes in the layout class to PinteresLayoutAttributes.

First, replace the following line:

private var cache = [UICollectionViewLayoutAttributes]()

with this one:

private var cache = [PinterestLayoutAttributes]()

Changing the reference to your new layout attributes introduces an error in prepareLayout().

To fix it, replace this line:

let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)

with these two lines:

let attributes = PinterestLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.photoHeight = photoHeight

This creates an instance of PinterestLayoutAttributes and then assigns the photoHeight property that will be passed to the collection view cells.

The last step is to change the image view height inside the cell.

Open AnnotatedPhotoCell.swift and add the following method to the bottom of the class:

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
  if let attributes = layoutAttributes as? PinterestLayoutAttributes {
    imageViewHeightLayoutConstraint.constant = attributes.photoHeight

First, this code calls the super implementation to make sure that the standard attributes are applied. Then, it casts the attributes object into an instance of PinterestLayoutAttributes to obtain the photo height and then changes the image view height by setting the imageViewHeightLayoutConstraint constant value.

Build and run your app. The contents of each cell should size properly and fill the entire space available to them:


You’ve now built a completely custom collection view layout – great work!

Where to Go From Here?

You can download the final project with all of the code from the tutorial.

With less work than you probably thought, you’ve created your very own Pinterest-like custom layout!

If you’re looking to learn more about custom layouts, consider the following resources:

If you have any questions or comments on this tutorial, feel free to join the discussion below in the forums!

The post UICollectionView Custom Layout Tutorial: Pinterest appeared first on Ray Wenderlich.



Write a comment