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.
Version
- Swift 4, iOS 11, Xcode 9

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.
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…
…or, you can check out some of the excellent tutorials on the site!
- UICollectionView Tutorial: Getting Started
- UICollectionView Tutorial: Reusable Views and Cell Selection
- UICollectionView Custom Layout Tutorial: Pinterest
- Video Tutorial: Collection Views
At the end of this UICollectionViewLayout
tutorial you’ll be able to implement a UICollectionView
like the following:
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:
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.
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:
- Subclass the abstract class
UICollectionViewLayout
and declare all the properties you’ll need to perform the layout calculations. - 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. - 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:
- An
enum
is a good choice for defining all the elements of theCustomLayout
. This prevents you from using strings. Remember the golden rule? No strings = no typos. - The
layoutAttributesClass
computed property provides the class to use for the attributes instances. You must return classes of typeCustomLayoutAttributes
: the custom class found in the starter project. - A subclass of
UICollectionViewLayout
must override thecollectionViewContentSize
computed property. - The
CustomLayout
needs all these properties in order to prepare the attributes. They’re allfileprivate
except thesettings
, sincesettings
could be set up by an external object. - 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
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:
- 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 newattributes
instances. - If the
cache
dictionary is empty, you have to properly initialize it. Do this by callingprepareCache()
. This will be implemented after this explanation. - 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 theCustomLayoutAttributes
class and then pass it toprepareElement(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 itsattributes
correctly. - The sticky menu is the second element of the collection view. You calculate its
attributes
the same way as before. - This loop is the most important of the core layout process. For every
item
in everysection
of the collection view you:- Create and prepare the
attributes
for the section's header. - Create the
attributes
for theitems
. - Associate them to a specific
indexPath
. - Calculate and set the items
frame
andzIndex
. - Update the
contentHeight
of theUICollectionView
. - Store the freshly created attributes in the
cache
dictionary using thetype
(in this case a cell) andindexPath
of the element as keys. - Finally, you create and prepare the
attributes
for the section's footer.
- Create and prepare the
- Last but not least, you call a method to update all
zIndex
values. You're going to discover details later aboutupdateZIndexes()
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:
- Check whether the element has a valid
size
or not. If the element has no size, there's no reason to cache itsattributes
- Next, assign the frame's
origin
value to the attribute'sinitialOrigin
property. Having a backup of the initial position of the element will be necessary in order to calculate the parallax and sticky transforms later. - Next, assign the
zIndex
value to prevent overlapping between different elements. - Once you've created and saved the required information, update the collection view's
contentHeight
since you've added a new element to yourUICollectionView
. A smart way to perform this update is by assigning the attribute's framemaxY
value to thecontentHeight
property. - Finally add the attributes to the
cache
dictionary using the elementtype
andindexPath
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:
- Inside
layoutAttributesForSupplementaryView(ofKind:at:)
you switch on the elementkind
property and return the cached attributes matching the correctkind
andindexPath
. - Inside
layoutAttributesForItem(at:)
you do exactly the same for the cells’s attributes. - Inside
layoutAttributesForElements(in:)
you empty thevisibleLayoutAttributes
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 itsframe
intersects the collection view’sframe
. Finally return thevisibleAttributes
array.
Step 3: Adopting 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:
Next, open the Identity Inspector and change the Custom Class to CustomLayout
as shown below:
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:
- First, register the custom classes used for the stretchy header and the custom menu. These are
UICollectionReusableView
subclasses already implemented in the starter project. - 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:
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:
- Make the header stretchy and bouncy.
- Add a sticky effect to the menu and the section headers.
- Implement a smooth parallax effect to make the user interface more engaging.
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:
- They let you create complex visual effects like translation, scaling and rotation, or a combination of the three, in very few lines of code.
- They interoperate in a flawless way with
UIKit
components andAutoLayout
. - 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:
- You store some useful values to avoid calculating them in the loop.
- This is the same loop as the previous version of this method. You iterate on all the cached attributes.
- Reset to the default value
parallax
transform and the element attributestransform
. - For the moment, you simply call a method to update the different kind of supplementary views. You'll implement it after this code block.
- 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:
- 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. - 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.
- 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:
- Calculate the distance of the cell from the
center
of the collection view. - Map proportionally the cell's distance from the center on the maximum
parallax
value (set in the layout settings) - Bound the
parallaxOffset
to avoid visual glitches. - Create a
CAAffineTransform
translation with the computedparallax
value. Finally, assign the translation to the cell's attributestransform
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).
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! :]
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)
Comments