Custom UICollectionViewLayout Tutorial With Parallax

Introduced in iOS6, UICollectionView is a first-class choice for advanced customization and animation. Learn more in this UICollectionViewLayout tutorial. By .

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Step 2: Implementing the CollectionViewLayout Core Process

Note: If you’re not familiar with the Core Layout process take a moment and read this tutorial on custom layout from our site. The following code requires a clear understanding of the Core Layout workflow.

The collection view works directly with your CustomLayout object to manage the overall layout process. For example, the collection view asks for layout information when it’s first displayed or resized.

During the layout process, the collection view calls the required methods of the CustomLayout object. Other optional methods may be called under specific circumstances like animated updates. These methods are your chance to calculate the position of items and to provide the collection view with the information it needs.

The first two required methods to override are:

  • prepare()
  • shouldInvalidateLayout(forBoundsChange:)

prepare() is your opportunity to perform whatever calculations are needed to determine the position of the elements in the layout. shouldInvalidateLayout(forBoundsChange:) is where you define how and when the CustomLayout object needs to perform the core process again.

Let’s start by implementing prepare().

Open CustomLayout.swift and add the following extension to the end of the file:

// MARK: - LAYOUT CORE PROCESS
extension CustomLayout {

  override public func prepare() {
    
    // 1
    guard let collectionView = collectionView,
      cache.isEmpty else {
      return
    }
    // 2
    prepareCache()
    contentHeight = 0
    zIndex = 0
    oldBounds = collectionView.bounds
    let itemSize = CGSize(width: cellWidth, height: cellHeight)
    
    // 3
    let headerAttributes = CustomLayoutAttributes(
      forSupplementaryViewOfKind: Element.header.kind,
      with: IndexPath(item: 0, section: 0)
    )
    prepareElement(size: headerSize, type: .header, attributes: headerAttributes)
    
    // 4
    let menuAttributes = CustomLayoutAttributes(
      forSupplementaryViewOfKind: Element.menu.kind,
      with: IndexPath(item: 0, section: 0))
    prepareElement(size: menuSize, type: .menu, attributes: menuAttributes)
    
    // 5
    for section in 0 ..< collectionView.numberOfSections {

      let sectionHeaderAttributes = CustomLayoutAttributes(
        forSupplementaryViewOfKind: UICollectionElementKindSectionHeader,
        with: IndexPath(item: 0, section: section))
      prepareElement(
        size: sectionsHeaderSize,
        type: .sectionHeader,
        attributes: sectionHeaderAttributes)

      for item in 0 ..< collectionView.numberOfItems(inSection: section) {
        let cellIndexPath = IndexPath(item: item, section: section)
        let attributes = CustomLayoutAttributes(forCellWith: cellIndexPath)
        let lineInterSpace = settings.minimumLineSpacing
        attributes.frame = CGRect(
          x: 0 + settings.minimumInteritemSpacing,
          y: contentHeight + lineInterSpace,
          width: itemSize.width,
          height: itemSize.height
        )
        attributes.zIndex = zIndex
        contentHeight = attributes.frame.maxY
        cache[.cell]?[cellIndexPath] = attributes
        zIndex += 1
      }

      let sectionFooterAttributes = CustomLayoutAttributes(
        forSupplementaryViewOfKind: UICollectionElementKindSectionFooter,
        with: IndexPath(item: 1, section: section))
      prepareElement(
        size: sectionsFooterSize,
        type: .sectionFooter,
        attributes: sectionFooterAttributes)
    }
    
    // 6
    updateZIndexes()
  }
}

Taking each commented section in turn:

  • Create and prepare the attributes for the section's header.
  • Create the attributes for the items.
  • Associate them to a specific indexPath.
  • Calculate and set the items frame and zIndex.
  • Update the contentHeight of the UICollectionView.
  • Store the freshly created attributes in the cache dictionary using the type (in this case a cell) and indexPath of the element as keys.
  • Finally, you create and prepare the attributes for the section's footer.
  1. Prepare operations are resourse-intensive and could impact performance. For this reason, you’re going to cache the calculated attributes on creation. Before executing, you have to check whether the cache dictionary is empty or not. This is crucial to not to mess up old and new attributes instances.
  2. If the cache dictionary is empty, you have to properly initialize it. Do this by calling prepareCache(). This will be implemented after this explanation.
  3. The stretchy header is the first element of the collection view. For this reason, you take into account its attributes first. You create an instance of the CustomLayoutAttributes class and then pass it to prepareElement(size:type:attributes). Again, you’ll implement this method later. For the moment keep in mind each time you create a custom element you have to call this method in order to cache its attributes correctly.
  4. The sticky menu is the second element of the collection view. You calculate its attributes the same way as before.
  5. This loop is the most important of the core layout process. For every item in every section of the collection view you:
  6. Last but not least, you call a method to update all zIndex values. You're going to discover details later about updateZIndexes() and you'll learn why it’s important to do that.

Next, add the following method just below prepare():

override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
  if oldBounds.size != newBounds.size {
    cache.removeAll(keepingCapacity: true)
  }
  return true
}

Inside shouldInvalidateLayout(forBoundsChange:), you have to define how and when you want to invalidate the calculations performed by prepare(). The collection view calls this method every time its bounds property changes. Note that the collection view's bounds property changes every time the user scrolls.

You always return true and if the bounds size changes, which means the collection view transited from portrait to landscape mode or vice versa, you purge the cache dictionary too.

A cache purge is necessary because a change of the device’s orientation triggers a redrawing of the collection view’s frame. As a consequence all the stored attributes won’t fit inside the new collection view's frame.

Next, you're going to implement all the methods called inside prepare() but haven't yet implemented:

Add the following to the bottom of the extension:

private func prepareCache() {
  cache.removeAll(keepingCapacity: true)
  cache[.header] = [IndexPath: CustomLayoutAttributes]()
  cache[.menu] = [IndexPath: CustomLayoutAttributes]()
  cache[.sectionHeader] = [IndexPath: CustomLayoutAttributes]()
  cache[.sectionFooter] = [IndexPath: CustomLayoutAttributes]()
  cache[.cell] = [IndexPath: CustomLayoutAttributes]()
}

This first thing this method does is empty the cache dictionary. Next, it resets all the nested dictionaries, one for each element family, using the element type as primary key. The indexPath will be the secondary key used to identify the cached attributes.

Next, you're going to implement prepareElement(size:type:attributes:).

Add the following definition to the end of the extension:

private func prepareElement(size: CGSize, type: Element, attributes: CustomLayoutAttributes) {
  //1
  guard size != .zero else {
    return
  }
  //2
  attributes.initialOrigin = CGPoint(x:0, y: contentHeight)
  attributes.frame = CGRect(origin: attributes.initialOrigin, size: size)
  // 3
  attributes.zIndex = zIndex
  zIndex += 1
  // 4
  contentHeight = attributes.frame.maxY
  // 5
  cache[type]?[attributes.indexPath] = attributes
}

Here's a step-by-step explanation of what's happening above:

  1. Check whether the element has a valid size or not. If the element has no size, there's no reason to cache its attributes
  2. Next, assign the frame's origin value to the attribute's initialOrigin property. Having a backup of the initial position of the element will be necessary in order to calculate the parallax and sticky transforms later.
  3. Next, assign the zIndex value to prevent overlapping between different elements.
  4. Once you've created and saved the required information, update the collection view's contentHeight since you've added a new element to your UICollectionView. A smart way to perform this update is by assigning the attribute's frame maxY value to the contentHeight property.
  5. Finally add the attributes to the cache dictionary using the element type and indexPath as unique keys.

Finally it’s time to implement updateZIndexes() called at the end of prepare().

Add the following to the bottom of the extension:

private func updateZIndexes(){
  guard let sectionHeaders = cache[.sectionHeader] else {
    return
  }
  var sectionHeadersZIndex = zIndex
  for (_, attributes) in sectionHeaders {
    attributes.zIndex = sectionHeadersZIndex
    sectionHeadersZIndex += 1
  }
  cache[.menu]?.first?.value.zIndex = sectionHeadersZIndex
}

This methods assigns a progressive zIndex value to the section headers. The count starts from the last zIndex assigned to a cell. The greatest zIndex value is assigned to the menu's attributes. This re-assignment is necessary to have a consistent sticky behaviour. If this method isn't called, the cells of a given section will have a greater zIndex than the header of the section. This will cause ugly overlapping effects while scrolling.

To complete the CustomLayout class and make the layout core process work correctly, you need to implement some more required methods:

  • layoutAttributesForSupplementaryView(ofKind:at:)
  • layoutAttributesForItem(at:)
  • layoutAttributesForElements(in:)

The goal of these methods is to provide the right attributes to the right element at the right time. More specifically, the two first methods provide the collection view with the attributes for a specific supplementary view or a specific cell. The third one returns the layout attributes for the displayed elements in a given moment.

//MARK: - PROVIDING ATTRIBUTES TO THE COLLECTIONVIEW
extension CustomLayout {
  
  //1
  public override func layoutAttributesForSupplementaryView(
    ofKind elementKind: String,
    at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    
  switch elementKind {
    case UICollectionElementKindSectionHeader:
      return cache[.sectionHeader]?[indexPath]
      
    case UICollectionElementKindSectionFooter:
      return cache[.sectionFooter]?[indexPath]
      
    case Element.header.kind:
      return cache[.header]?[indexPath]
      
    default:
      return cache[.menu]?[indexPath]
    }
  }
  
  //2
  override public func layoutAttributesForItem(
    at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
      return cache[.cell]?[indexPath]
  }

  //3
  override public func layoutAttributesForElements(
    in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
      visibleLayoutAttributes.removeAll(keepingCapacity: true)
      for (_, elementInfos) in cache {
        for (_, attributes) in elementInfos where attributes.frame.intersects(rect) {
          visibleLayoutAttributes.append(attributes)
        }
      }
      return visibleLayoutAttributes
  }
}

Taking it comment-by-comment:

  1. Inside layoutAttributesForSupplementaryView(ofKind:at:) you switch on the element kind property and return the cached attributes matching the correct kind and indexPath.
  2. Inside layoutAttributesForItem(at:) you do exactly the same for the cells’s attributes.
  3. Inside layoutAttributesForElements(in:) you empty the visibleLayoutAttributes array (where you’ll store the visibile attributes). Next, iterate on all cached attributes and add only visible elements to the array. To determinate whether an element is visibile or not, test if its frame intersects the collection view’s frame. Finally return the visibleAttributes array.

Contributors

Darren Ferguson

Tech Editor

Chris Belanger

Editor

Andy Obusek

Final Pass Editor and Team Lead

Over 300 content creators. Join our team.