Custom UICollectionViewLayout Tutorial With Parallax

Paride Broggi
UICollectionViewLayout tutorial

Let it scroll! Let it scroll!

Note: This tutorial uses Xcode 9.0 and Swift 4.

Introduced in iOS 6 and refined with new features in iOS 10, UICollectionView is the first-class choice to customize and animate the presentation of data collections in iOS applications.

A key entity associated with UICollectionView is the UICollectionViewLayout. The UICollectionViewLayout object is responsible for defining the attributes of all the elements of a collection view such as cells, supplementary views and decoration views.

UIKit offers a default implementation of UICollectionViewLayout called UICollectionViewFlowLayout. This class lets you set up a grid layout with some elementary customizations.

This UICollectionViewLayout tutorial will teach you how to subclass and customize the UICollectionViewLayout class. It will also show you how to add custom supplementary views, stretchy, sticky and parallax effects to a collection view.

Note: This UICollectionViewLayout tutorial requires an intermediate knowledge of Swift 4.0, an advanced knowledge of UICollectionView, affine transforms and a clear understanding of how the core layout process works in the UICollectionViewLayout class.

If you’re unfamiliar with any of these topics, you could read the Apple official documentation…

UICollectionViewLayout tutorial

…or, you can check out some of the excellent tutorials on the site!

At the end of this UICollectionViewLayout tutorial you’ll be able to implement a UICollectionView like the following:

UICollectionViewLayout tutorial

Are you ready to win the Jungle Cup? Let’s go!

Getting Started

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

You’ll see some cute owls laid out in a standard UICollectionView with sections headers and footers like the following:

UICollectionViewLayout tutorial

The app presents the Owls Team’s players who are taking part in the Jungle Soccer Cup 2017. Section headers show their roles in the team while footers display their collective strength.

Let’s have a closer look at the starter project:

Inside JungleCupCollectionViewController.swift file you’ll find the implementation of a UICollectionViewController subclass conforming to the UICollectionDataSource protocol. It implements all the required methods plus the optional method for adding supplementary views.

The JungleCupCollectionViewController adopts MenuViewDelegate too. It’s a protocol to let the collection view switch its data source.

In the Reusable Views folder, there are subclasses of UICollectionViewCell for the cells, and UICollectionReusableView for section header and section footer views. They link to their respective views designed in the Main.storyboard file.

Besides that, there are the custom supplementary views the CustomLayout requires. Both the HeaderView and MenuView classes are subclasses of UICollectionReusableView. They’re both linked to their own .xib files.

MockDataManager.swift file holds the data structures for all the teams. For convenience’s sake, the Xcode project embeds all the necessary assets.

Layout Settings

The Custom Layout folder deserves special attention because it contains two important files:

  • CustomLayoutSettings.swift
  • CustomLayoutAttributes.swift

CustomLayoutSettings.swift implements a structure with all the layout settings. The first group of settings deals with collection view’s elements sizes. The second group defines the layout behaviors, and the third sets up the layout spacings.

Layout Attributes

The CustomLayoutAttributes.swift file implements a UICollectionViewLayoutAttributes subclass named CustomLayoutAttributes. This class stores all the information the collection view needs to configure an element before displaying it.

It inherits the default attributes such as frame, transform, transform3D, alpha and zIndex from the superclass.

It also adds some new custom properties:

  
  var parallax: CGAffineTransform = .identity
  var initialOrigin: CGPoint = .zero
  var headerOverlayAlpha = CGFloat(0)

parallax, initialOrigin and headerOverlayAlpha are custom properties you’ll use later in the implementation of stretchy and sticky effects.

Note: Layout attributes objects may be copied by the collection view. Thus, when subclassing UICollectionViewLayoutAttributes, you must conform to NSCopying by implementing an appropriate method for copying your custom attributes to new instances.

If you implement custom layout attributes, you must also override the inherited isEqual method to compare the values of your properties. Starting with iOS 7, the collection view does not apply layout attributes if those attributes have not changed.

Currently the collection view can’t display all the teams yet. For the moment, supporters of Tigers, Parrots and Giraffes have to wait.

No worries. They will be back soon! CustomLayout will solve the problem :]

The Role of UICollectionViewLayout

The main goal of a UICollectionViewLayout object is to provide information about the position and visual state of every element in a UICollectionView. Please keep in mind a UICollectionViewLayout object isn’t responsible for creating the cells or supplementary views. Its job is to provide them with the right attributes.

Creating a custom UICollectionViewLayout is a three-step process:

  1. Subclass the abstract class UICollectionViewLayout and declare all the properties you’ll need to perform the layout calculations.
  2. Perform all needed calculations to provide every collection view’s element with the right attributes. This part will be the most complex because you’re going to implement the CollectionViewLayout core process from scratch.
  3. Make the collection view adopt the new CustomLayout class.

Step 1: Subclassing the UICollectionViewLayout Class

Inside the Custom Layout group you can find a Swift file named CustomLayout.swift which contains a CustomLayout class stub. Within this class you’ll implement the UICollectionViewLayout subclass and all the Core Layout processes.

First, declare all the properties CustomLayout needs to calculate the attributes.

import UIKit

final class CustomLayout: UICollectionViewLayout {
  
  // 1
  enum Element: String {
    case header
    case menu
    case sectionHeader
    case sectionFooter
    case cell
    
    var id: String {
      return self.rawValue
    }
    
    var kind: String {
      return "Kind\(self.rawValue.capitalized)"
    }
  }
  
  // 2
  override public class var layoutAttributesClass: AnyClass {
    return CustomLayoutAttributes.self
  }
  
  // 3
  override public var collectionViewContentSize: CGSize {
    return CGSize(width: collectionViewWidth, height: contentHeight)
  }

  // 4
  var settings = CustomLayoutSettings()
  private var oldBounds = CGRect.zero
  private var contentHeight = CGFloat()
  private var cache = [Element: [IndexPath: CustomLayoutAttributes]]()
  private var visibleLayoutAttributes = [CustomLayoutAttributes]()
  private var zIndex = 0
  
  // 5
  private var collectionViewHeight: CGFloat {
    return collectionView!.frame.height
  }

  private var collectionViewWidth: CGFloat {
    return collectionView!.frame.width
  }

  private var cellHeight: CGFloat {
    guard let itemSize = settings.itemSize else {
      return collectionViewHeight
    }

    return itemSize.height
  }

  private var cellWidth: CGFloat {
    guard let itemSize = settings.itemSize else {
      return collectionViewWidth
    }

    return itemSize.width
  }

  private var headerSize: CGSize {
    guard let headerSize = settings.headerSize else {
      return .zero
    }

    return headerSize
  }

  private var menuSize: CGSize {
    guard let menuSize = settings.menuSize else {
      return .zero
    }

    return menuSize
  }

  private var sectionsHeaderSize: CGSize {
    guard let sectionsHeaderSize = settings.sectionsHeaderSize else {
      return .zero
    }

    return sectionsHeaderSize
  }

  private var sectionsFooterSize: CGSize {
    guard let sectionsFooterSize = settings.sectionsFooterSize else {
      return .zero
    }

    return sectionsFooterSize
  }

  private var contentOffset: CGPoint {
    return collectionView!.contentOffset
  }
}

That’s a fair chunk of code, but it’s fairly straightforward once you break it down:

  1. An enum is a good choice for defining all the elements of the CustomLayout. This prevents you from using strings. Remember the golden rule? No strings = no typos.
  2. The layoutAttributesClass computed property provides the class to use for the attributes instances. You must return classes of type CustomLayoutAttributes: the custom class found in the starter project.
  3. A subclass of UICollectionViewLayout must override the collectionViewContentSize computed property.
  4. The CustomLayout needs all these properties in order to prepare the attributes. They’re all fileprivate except the settings, since settings could be set up by an external object.
  5. Computed properties used as syntactic sugar to avoid verbose repetitions later.

Now that you’re done with declarations, you can focus on the Core Layout process implementation.

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:

  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:
    • 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.
  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.

Step 3: Adoptng the CustomLayout

Before building and running the project you need to:

  • Make the collection view adopt the CustomLayout class.
  • Make the JungleCupCollectionViewController support the custom supplementary views.

Open Main.storyboard and select the Collection View Flow Layout in the Jungle Cup Collection View Controller Scene as shown below:

UICollectionViewLayout tutorial

Next, open the Identity Inspector and change the Custom Class to CustomLayout as shown below:

UICollectionViewLayout tutorial

Next, open JungleCupCollectionViewController.swift.

Add the computed property customLayout to avoid verbose code duplication.

Your code should look like the following:

var customLayout: CustomLayout? {
  return collectionView?.collectionViewLayout as? CustomLayout
}

Next, replace setUpCollectionViewLayout() with the following:

 
  private func setupCollectionViewLayout() {
    guard let collectionView = collectionView,
      let customLayout = customLayout else {
        return
    }
    // 1
    collectionView.register(
        UINib(nibName: "HeaderView", bundle: nil),
        forSupplementaryViewOfKind: CustomLayout.Element.header.kind,
        withReuseIdentifier: CustomLayout.Element.header.id
    )
    collectionView.register(
        UINib(nibName: "MenuView", bundle: nil),
        forSupplementaryViewOfKind: CustomLayout.Element.menu.kind,
        withReuseIdentifier: CustomLayout.Element.menu.id
    )
    
    // 2
    customLayout.settings.itemSize = CGSize(width: collectionView.frame.width, height: 200)
    customLayout.settings.headerSize = CGSize(width: collectionView.frame.width, height: 300)
    customLayout.settings.menuSize = CGSize(width: collectionView.frame.width, height: 70)
    customLayout.settings.sectionsHeaderSize = CGSize(width: collectionView.frame.width, height: 50)
    customLayout.settings.sectionsFooterSize = CGSize(width: collectionView.frame.width, height: 50)
    customLayout.settings.isHeaderStretchy = true
    customLayout.settings.isAlphaOnHeaderActive = true
    customLayout.settings.headerOverlayMaxAlphaValue = CGFloat(0)
    customLayout.settings.isMenuSticky = true
    customLayout.settings.isSectionHeadersSticky = true
    customLayout.settings.isParallaxOnCellsEnabled = true
    customLayout.settings.maxParallaxOffset = 60
    customLayout.settings.minimumInteritemSpacing = 0
    customLayout.settings.minimumLineSpacing = 3
}

Here's what the code above does:

  1. First, register the custom classes used for the stretchy header and the custom menu. These are UICollectionReusableView subclasses already implemented in the starter project.
  2. Finally, set sizes, behaviours and spacings of the CustomLayout settings.

Before you build an run the app, add the following two case options to viewForSupplementaryElementOfKind(_:viewForSupplementaryElementOfKind:at:) to handle custom supplementary view types:

case CustomLayout.Element.header.kind:
  let topHeaderView = collectionView.dequeueReusableSupplementaryView(
    ofKind: kind,
    withReuseIdentifier: CustomLayout.Element.header.id,
    for: indexPath)
  return topHeaderView
      
case CustomLayout.Element.menu.kind:
  let menuView = collectionView.dequeueReusableSupplementaryView(
    ofKind: kind,
    withReuseIdentifier: CustomLayout.Element.menu.id,
    for: indexPath)
  if let menuView = menuView as? MenuView {
    menuView.delegate = self
  }
  return menuView

Well done! It was a long journey, but you're almost done.

Build and run the project! You should see something similar to the following:

UICollectionViewLayout tutorial

The UICollectionView from the starter project now has some extra features:

  • At the top there's a big header showing the Jungle Cup's logo.
  • Below that, there's a menu with four buttons, one for each team. If you tap a button, the collection view reloads with the corresponding team.

You've already done a good job, but you can do better. It’s time to go for some nice visual effects to dress up your UICollectionView.

Adding Stretchy, Sticky and Parallax Effects

In the final section of this UICollectionViewLayout tutorial, you're going to add the following visual effects:

  1. Make the header stretchy and bouncy.
  2. Add a sticky effect to the menu and the section headers.
  3. Implement a smooth parallax effect to make the user interface more engaging.
Note: If you’re not familiar with CGATransform, you can check out this tutorial before continuing. The following part of the UICollectionViewLayout tutorial implies a basic knowledge of affine transforms.

Affine Transforms

The Core Graphics CGAffineTransform API is the best way to apply visual effects to the elements of a UICollectionView.

Affine transforms are quite useful for a variety of reasons:

  1. They let you create complex visual effects like translation, scaling and rotation, or a combination of the three, in very few lines of code.
  2. They interoperate in a flawless way with UIKit components and AutoLayout.
  3. They help you keep performance optimal even in complicated scenarios.

The math behind affine transforms is really cool. However, explaining how matrices work behind the scenes of CGATransform is out of scope for this UICollectionViewLayout tutorial.

If you’re interested in this topic, you can find more details in Apple’s Core Graphic Framework Documentation.

Transforming Visible Attributes

Open CustomLayout.swift and update layoutAttributesForElements(in:) to the following:

override public func layoutAttributesForElements(
  in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    guard let collectionView = collectionView else {
      return nil
    }
    visibleLayoutAttributes.removeAll(keepingCapacity: true)
    // 1
    let halfHeight = collectionViewHeight * 0.5
    let halfCellHeight = cellHeight * 0.5
    // 2
    for (type, elementInfos) in cache {
      for (indexPath, attributes) in elementInfos {
        // 3
        attributes.parallax = .identity
        attributes.transform = .identity
        // 4
        updateSupplementaryViews(
          type,
          attributes: attributes,
          collectionView: collectionView,
          indexPath: indexPath)
        if attributes.frame.intersects(rect) {
          // 5
          if type == .cell,
            settings.isParallaxOnCellsEnabled {
              updateCells(attributes, halfHeight: halfHeight, halfCellHeight: halfCellHeight)
          }
          visibleLayoutAttributes.append(attributes)
        }
      }
    }
    return visibleLayoutAttributes
}

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

  1. You store some useful values to avoid calculating them in the loop.
  2. This is the same loop as the previous version of this method. You iterate on all the cached attributes.
  3. Reset to the default value parallax transform and the element attributes transform.
  4. For the moment, you simply call a method to update the different kind of supplementary views. You'll implement it after this code block.
  5. Check whether the current attributes belong to a cell. If the parallax effect is activated in the layout settings, call a method to update its attributes. Just as above, you'll implement this method after this code block.

Next, it's time to implement the two methods called in the above loop:

  • updateSupplementaryViews(_:attributes:collectionView:indexPath:)
  • updateCells(_:halfHeight:halfCellHeight:)

Add the following:

private func updateSupplementaryViews(_ type: Element,
                                      attributes: CustomLayoutAttributes, 
                                      collectionView: UICollectionView,
                                      indexPath: IndexPath) {
    // 1
    if type == .sectionHeader,
      settings.isSectionHeadersSticky {
        let upperLimit = 
           CGFloat(collectionView.numberOfItems(inSection: indexPath.section))
           * (cellHeight + settings.minimumLineSpacing)
        let menuOffset = settings.isMenuSticky ? menuSize.height : 0
        attributes.transform =  CGAffineTransform(
          translationX: 0,
          y: min(upperLimit,
          max(0, contentOffset.y - attributes.initialOrigin.y + menuOffset)))
    }
    // 2
    else if type == .header,
      settings.isHeaderStretchy {
        let updatedHeight = min(
          collectionView.frame.height,
          max(headerSize.height, headerSize.height - contentOffset.y))
        let scaleFactor = updatedHeight / headerSize.height
        let delta = (updatedHeight - headerSize.height) / 2
        let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
        let translation = CGAffineTransform(
          translationX: 0,
          y: min(contentOffset.y, headerSize.height) + delta)
        attributes.transform = scale.concatenating(translation)
        if settings.isAlphaOnHeaderActive {
          attributes.headerOverlayAlpha = min(
            settings.headerOverlayMaxAlphaValue,
            contentOffset.y / headerSize.height)
        }
    }
    // 3
    else if type == .menu,
      settings.isMenuSticky {
        attributes.transform = CGAffineTransform(
          translationX: 0,
          y: max(attributes.initialOrigin.y, contentOffset.y) - headerSize.height)
    }
  }

Taking each numbered comment in turn:

  1. Test whether the current element is a section header. Then, if the sticky behaviour is activated in the layout settings, compute the transform. Finally assign the calculated value to the attributes' transform property.
  2. Same routine as above, but this time check whether the element is the top header. If the stretchy effect is activated, perform the transform calculations.
  3. Same routine again. This time perform transform calculations for the sticky menu.

Now it's time to transform the collection view cells:

  
private func updateCells(_ attributes: CustomLayoutAttributes,
                         halfHeight: CGFloat,
                         halfCellHeight: CGFloat) {
  // 1
  let cellDistanceFromCenter = attributes.center.y - contentOffset.y - halfHeight
    
  // 2
  let parallaxOffset = -(settings.maxParallaxOffset * cellDistanceFromCenter)
    / (halfHeight + halfCellHeight)
  // 3 
  let boundedParallaxOffset = min(
    max(-settings.maxParallaxOffset, parallaxOffset),
    settings.maxParallaxOffset)
  // 4
  attributes.parallax = CGAffineTransform(translationX: 0, y: boundedParallaxOffset)
}

Here's the play-by-play:

  1. Calculate the distance of the cell from the center of the collection view.
  2. Map proportionally the cell's distance from the center on the maximum parallax value (set in the layout settings)
  3. Bound the parallaxOffset to avoid visual glitches.
  4. Create a CAAffineTransform translation with the computed parallax value. Finally, assign the translation to the cell's attributes transform property.

To achieve the parallax effect on the PlayerCell, the image's frame should have top and bottom negative insets. In the starter project these constraints are set for you. You can check them in the Constraint inspector (see below).

UICollectionViewLayout tutorial

Before building, you have to fix one final detail. Open JungleCupCollectionViewController.swift. Inside setupCollectionViewLayout() change the following value:

customLayout.settings.headerOverlayMaxAlphaValue = CGFloat(0)

to the following:

customLayout.settings.headerOverlayMaxAlphaValue = CGFloat(0.6)

This value represents the maximum opacity value the layout can assign to the black overlay on the headerView.

Build and run the project to appreciate all the visual effects. Let it scroll! Let it scroll! Let it scroll! :]

UICollectionViewLayout tutorial

Where to Go From Here?

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

With a bit of code and some basic transforms, you’ve created a fully custom and settable UICollectionViewLayout you can reuse in your future projects for any need or purpose!

If you’re looking to learn more about custom UICollectionViewLayout, consider reading Creating Custom Layouts section of the Collection View Programming Guide for iOS, which covers this subject extensively.

I hope you enjoyed this UICollectionViewLayout tutorial! If you have any questions or comments feel free to join the discussion below in the forums.

(Credit for the vectorial animals used in the Jungle Cup logo go to: www.freevector.com)

Team

Each tutorial at www.raywenderlich.com is created by a team of dedicated developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Paride Broggi

Product innovation + iOS engineer @blackpills, philosophy graduate, minimalist.

Other Items of Interest

Big Book SaleAll raywenderlich.com iOS 11 books on sale for a limited time!

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

iOS Team

... 71 total!

Android Team

... 16 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 18 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!