UICollectionView Custom Layout Tutorial: A Spinning Wheel
In this UICollectionView custom layout tutorial, you’ll learn how create a spinning navigation layout, including managing view rotation and scrolling. By Rounak Jain.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
UICollectionView Custom Layout Tutorial: A Spinning Wheel
20 mins
There are some really creative websites on the Internet, and a few weeks ago, I came across one such website called Form Follows Function, which is a collection of different kinds of interactive experiences. What really caught my attention was the site’s spinning navigation wheel, which contained posters that represented each kind of experience.
This tutorial will show you how to use a UICollectionView
custom layout to recreate this spinning navigation wheel. To get the most out of your time here, you’ll need to have basic knowledge of 2D transforms, collection views and custom layouts. If you’re unfamiliar with any of these topics then I recommend you check out the following before continuing:
- UICollectionView Tutorial Part 1: Getting Started
- UICollectionView Tutorial Part 2: Reusable Views and Cell Selection
- Video Series: Collection Views
- Video Series: Custom Collection View Layouts
By the end of this tutorial, you’ll know how to:
- Create your own collection view layout from scratch, without using
UICollectionViewFlowLayout
as your base class - Rotate views around a point outside their bounds
And much, much more! Time to jump in.
Getting Started
First, download the starter project for this tutorial, open it in Xcode, and build and run. You’ll see a grid of cells, each representing a book from the raywenderlich.com store:
The project’s setup is fairly straight forward. There’s CollectionViewController
, and a custom collection view cell with an image view inside of it. The book covers are in a directory called Images, and CollectionViewController
populates the collection view using the directory as its data source.
Your task is to create a UICollectionViewLayout
subclass to lay these cells out in a circular fashion.
Theory
Here’s a diagram of the wheel structure along with the cells. The yellow area is the iPhone’s screen, the blue rounded rectangles are the cells, and the dotted line is the circle you’ll place them around:
You’ll need three main parameters to describe this arrangement:
- The radius of the circle (
radius
); - The angle between each cell (
anglePerItem
); - The angular position of cells.
As you probably noticed, not all the cells fit within the screen’s bounds.
Assume that the 0th cell has an angle of x degrees, then the 1st cell will have an angular position of x + anglePerItem
, the second x + (2 * anglePerItem)
and so on. This can be generalized for the nth item as:
angle_for_i = x + (i * anglePerItem)
Below, you’ll see a depiction of the angular coordinate system. An angle of 0 degrees refers to the center, while positive angles are shown towards the right and negative are towards the left. So a cell with an angle of 0 will lie in the center — completely vertical.
Now that you’re clear on the underlying theories, you’re ready to start coding!
Circular Collection View Layout
Create a new Swift file with the iOS\Source\Cocoa Touch Class template. Name it CircularCollectionViewLayout, and make it a subclass of UICollectionViewLayout
:
Click Next, and then Create. This collection view layout subclass will contain all the positioning code.
As this is a subclass of UICollectionViewLayout
rather than UICollectionViewFlowLayout
, you’ll have to handle all parts of the layout process yourself instead of piggybacking the parents implementation using calls to super
.
On that note, I find that flow layout is well suited for grids, but not for circular layouts.
In CircularCollectionViewLayout
, create properties for itemSize
and radius
:
let itemSize = CGSize(width: 133, height: 173)
var radius: CGFloat = 500 {
didSet {
invalidateLayout()
}
}
When the radius changes, you recalculate everything, hence the call to invalidateLayout()
inside didSet
.
Below the radius
declaration, define anglePerItem
:
var anglePerItem: CGFloat {
return atan(itemSize.width / radius)
}
anglePerItem
can be any value you want, but this formula ensures that the cells aren’t spread too far apart.
Next, implement collectionViewContentSize()
to declare how big the content of your collection view should be:
override func collectionViewContentSize() -> CGSize {
return CGSize(width: CGFloat(collectionView!.numberOfItemsInSection(0)) * itemSize.width,
height: CGRectGetHeight(collectionView!.bounds))
}
The height will be the same as the collection view, but its width will be itemSize.width * numberOfItems
.
Now, open Main.storyboard, select Collection View in the document outline:
Open the Attributes Inspector and change Layout to Custom, and Class to CircularCollectionViewLayout
:
Build and run. Apart from a scrollable area, you won’t see anything, but that’s exactly what you want to see! It confirms that you’ve correctly told the collection view to use CircularCollectionViewLayout
as its layout class.
Custom Layout Attributes
Along with a collection view layout subclass, you’ll also need to subclass UICollectionViewLayoutAttributes
to store the angular position and anchorPoint
.
Add the following code to CircularCollectionViewLayout.swift, just above the CircularCollectionViewLayout
class declaration:
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
// 1
var anchorPoint = CGPoint(x: 0.5, y: 0.5)
var angle: CGFloat = 0 {
// 2
didSet {
zIndex = Int(angle * 1000000)
transform = CGAffineTransformMakeRotation(angle)
}
}
// 3
override func copyWithZone(zone: NSZone) -> AnyObject {
let copiedAttributes: CircularCollectionViewLayoutAttributes =
super.copyWithZone(zone) as! CircularCollectionViewLayoutAttributes
copiedAttributes.anchorPoint = self.anchorPoint
copiedAttributes.angle = self.angle
return copiedAttributes
}
}
- You need
anchorPoint
because the rotation happens around a point that isn’t the center. - While setting
angle
, you internally settransform
to be equal to a rotation ofangle
radians. You also want cells on the right to overlap the ones to their left, so you setzIndex
to a function that increases inangle
. Sinceangle
is expressed in radians, you amplify its value by 1,000,000 to ensure that adjacent values don’t get rounded up to the same value ofzIndex
, which is anInt
. - This overrides
copyWithZone()
. Subclasses of UICollectionViewLayoutAttributes need to conform to theNSCopying
protocol because the attribute’s objects can be copied internally when the collection view is performing a layout. You override this method to guarantee that both theanchorPoint
andangle
properties are set when the object is copied.
Now, jump back to CircularCollectionViewLayout
and implement layoutAttributesClass()
:
override class func layoutAttributesClass() -> AnyClass {
return CircularCollectionViewLayoutAttributes.self
}
This tells the collection view that you’ll be using CircularCollectionViewLayoutAttributes
, and not the default UICollectionViewLayoutAttributes
for your layout attributes.
To hold layout attributes instances, create an array called attributesList
below all other property declarations in CircularCollectionViewLayout
:
var attributesList = [CircularCollectionViewLayoutAttributes]()