Swift Expanding Cells in iOS Collection Views

Learn how to make expanding cells as in the Ultravisual app in this tutorial on collection views.


  • Other, Other, Other

Expanding collection view cells!

When collection views were introduced in 2012, many people thought it was the answer to the roadblocks and limitations of customizing table views.

Out of the box, UICollectionView is a powerful and ultra-flexible way to present data in your apps. However, one downside is that without some level of customization, your app looks bland and fails to stand out among the millions of apps in the App Store.

In this tutorial, you’re going to take a rather plain “template” app of different colored items and turn it into a visually appeasing way to browse RWDevCon inspiration talk sessions.

You’ll add awesome parallax effects and a featured cell item, as well as dabble in subtle fading and transitions to make it really pop and stand out from the crowd. The end result is a look and feel similar to the UltraVisual app. If you haven’t seen it, get it now – it’s free!

Before beginning, you should know how to work with UICollectionViewController, or its close cousin UITableViewController, and understand the basics of Swift. If you need a refresher, check out our two-part UICollectionView tutorial for the basics.

Note from Ray: This is a written version of the Video Tutorial Series, Custom Collection View Layouts from RayWenderlich.com team member, Mic Pringle!

Getting Started

Download the Ultravisual Starter Kit project and open it in Xcode 6. Inside, you’ll find a simple storyboard project with several classes conveniently separated into their respective folders in order to remove some of the boilerplate code and setup details to get the app running.

The folders are broken out as follows:

  • Assets: Contains Images.xcassets, Inspirations.plist and Inspirations.xcassets, which are used to load the images for the RWDevCon speakers.
  • Controllers: Contains InspirationsViewController.swift, a UICollectionViewController subclass that sets up the collection view in the same way UITableViewController does.
  • Extensions: Contains UIColor+Palette.swift and UIImage+Decompression.swift, which provide convenience methods that handle color conversion and image decompression, respectively. The starter project uses the color palette to start off with, and in a future step, you’ll switch to using images and the decompression method.
  • Layouts: Contains UltravisualLayout.swift — the meat of the project. As a subclass of UICollectionViewLayout, UICollectionView will look to this class for a definition of how to properly lay out and place the items specified and provided by InspirationsViewController. Inside, you’ll also find a set of constants and convenience methods you’ll use to pre-calculate and simplify set up of the starter project. Additionally, a simple custom cache has been setup to cache UICollectionViewLayoutAttributes that are used to modify each cell.
  • Models: Contains the data models for the background images (Inspiration.swift) and session details (Session.swift); they are separated for the ultimate MVC-pattern win!
  • Views: Contains InspirationCell.swift, which handles setting the properties of the collection view cell, just like UITableViewCell.

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

Starter Kit Screenshot

The app looks decent (if you want a random color palette app, that is), but it doesn’t do much right now. That’s where your job comes in — you’ll turn this clunker into a beautiful app that’s designed to quickly scan through the list of inspirational speaker topics from RWDevCon.

The technical magic of this project comes from managing the different cell item states:

  • The standard cell (what you see in the starter project) that transitions into the “next up” cell which grows a bit larger
  • The featured cell, which is the largest of all.

You’ll also use a little trickery — modifying the z-index of the cell as it scrolls — for a sweet stacking effect.

And now it’s time to stretch your fingers and clear your mind in preparation of the amazing work you’re about to do. That’s right boys and girls, it’s…

Multiple Cell Sizes

The first thing you need to do is create the featured cell. It’ll be roughly twice as large as the standard cell, so a user can clearly see which item is selected.

Jump to UltravisualLayout.swift and go to prepareLayout(). After this line of code:

cache.removeAll(keepCapacity: false)

Add a few new local variables that you’ll use across each item:

let standardHeight = UltravisualLayoutConstants.Cell.standardHeight
let featuredHeight = UltravisualLayoutConstants.Cell.featuredHeight

var frame = CGRectZero
var y: CGFloat = 0    

Next, add the following code to loop through each item in the collection view and adjust the item’s state accordingly:

for item in 0..<numberOfItems {
  // 1
  let indexPath = NSIndexPath(forItem: item, inSection: 0)
  let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)

  // 2
  attributes.zIndex = item
  var height = standardHeight

  // 3
  if indexPath.item == featuredItemIndex {
    // 4
    let yOffset = standardHeight * nextItemPercentageOffset
    y = collectionView!.contentOffset.y - yOffset
    height = featuredHeight
  } else if indexPath.item == (featuredItemIndex + 1) && indexPath.item != numberOfItems {
    // 5 
    let maxY = y + standardHeight
    height = standardHeight + max((featuredHeight - standardHeight) * nextItemPercentageOffset, 0)
    y = maxY - height

  // 6
  frame = CGRect(x: 0, y: y, width: width, height: height)
  attributes.frame = frame
  y = CGRectGetMaxY(frame)

There is a lot going on in the loop so here's a breakdown:

  1. Create an index path to the current cell, then get its current attributes.
  2. Prepare the cell to move up or down. Since the majority of cells will not be featured -- there are many more standard cells than the single featured cells -- it defaults to the standardHeight.
  3. Determine the current cell's status -- featured, next or standard. In the case of the latter, you do nothing.
  4. If the cell is currently in the featured cell position, calculate the yOffset and use that to derive the new y value for the cell. After that, you set the cell's height to be the featured height.
  5. If the cell is next in line, you start by calculating the largest y could be (in this case, larger than the featured cell) and combine that with a calculated height to end up with the correct value of y, which is 280.0 -- the height of the featured cell.
  6. Lastly, the loop sets some common elements for each cell, including creating the right frame based upon the if condition above, setting the attributes to what was just calculated, and updating the cache values. The very last step is to update y so that it's at the bottom of the last calculated cell, so you can move down the list of cells efficiently.

For the collection view to know which layout to use, you'll need to tell UICollectionView to use UltravisualLayout as its layout class. Open Main.storyboard and select the Collection View inside of Document Outline.

Collection View Selection

Open the Attributes Inspector and set the Layout dropdown to Custom, and then set the Class to UltravisualLayout.

Screen Shot 2015-05-17 at 5.05.37 PM

Finally, before building, go to InspirationsViewController.swift and in viewDidLoad(), remove the last 2 lines:

let layout = collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSize(width: CGRectGetWidth(collectionView!.bounds), height: 100)

These two lines were the basic layout to show the equally sized multi-colored cells. Now that you've specified layout code in UltravisualLayout and set the custom layout class in the storyboard, you no longer need these lines.

Build and run, and you'll see this:


Notice that the top cell is much larger, effectively showcasing a featured cell. As you scroll, the cell below the featured cell expands and overlaps the current featured cell. What a nice effect!

Adding Cell Images

Colored cells are great and all, but RWDevCon cannot be expressed in color!

There is a set of images included in the starter project just begging to be used. PS: they're in Inspirations.xcassets if you want to steal a quick glance. :]

But you can't just use them in any old way -- if you're doing this tutorial, then clearly you're all about that awesome user experience.

Not only is it time to replace the colors with images, but you'll do so in such a way that the image actually reveals more of itself as the user scrolls the cell up to be a featured cell.

First, add a new image view to the collection view cell. Go to Main.storyboard, select InspirationCell, and resize it to 200x200. This is not strictly necessary but it helps you visually see what's going on more easily than the standard cell size does.

Screen Shot 2015-05-17 at 10.50.30 PM

Next, open the Identity Inspector and set the Class type to be the included InspirationCell.

Screen Shot 2015-05-17 at 10.53.55 PM

Then drag and drop a UIImageView object from the Object Library onto the cell. Select the UIImageView you just added, go to the Attributes Inspector and change the Mode to Aspect Fill.

Now for a few Auto Layout constraints -- select the Pin menu at the bottom of the layout view and set up the constraints as follows:

  • Uncheck Constrain to Margins
  • Set the leading and trailing spaces (the left and right boxes) to 0
  • Check Height constraint and set its value to 280 (the height of the featured cell)
  • In the Update Frames dropdown, select Items of New Constraints
  • Click Apply 3 New Constraints

It should appear something like this:

Screen Shot 2015-05-17 at 11.05.45 PM

From the Align menu (located to the left of the Pin menu), select the Vertical Center in Container option and apply that single constraint.

This creates a cool effect that centers the image in the image view in proportion to the full height of the featured cell, but it appears masked by the standard cell height until the cell is fully in view.

Screen Shot 2015-05-17 at 11.03.33 PM

Finally, add an outlet connection by right-clicking on InspirationCell in the Document Outline and connecting the imageView outlet to the UIImageView you just added.

Screen Shot 2015-05-17 at 11.08.29 PM

Jump over to InspirationsViewController.swift and update the implementation of collectionView(_:cellForItemAtIndexPath:) to the following:

let cell = collectionView.dequeueReusableCellWithReuseIdentifier("InspirationCell", forIndexPath: indexPath) as! InspirationCell
cell.inspiration = inspirations[indexPath.item]
return cell

Since you set the cell's class in Interface Builder, you start by casting to InspirationsCell in the dequeue call. Rather than set the cell's background color, you set the cell's inspiration property to the correct piece of data from the array of all inspirations.

Lastly, at the top of the file, remove the colors array because you won't be needing it anymore.

Build and run, and behold pure image glory!

iOS Simulator Screen Shot May 17, 2015, 11.16.04 PM

Featuring Featured Cells

The new images look great, but one thing you'll notice is that all of the images are competing for your attention regardless of where they are in the list.

In reality, only the featured cell should grab your attention.

Next, you'll add a simple UIView overlay to manage the opacity of each cell. This time, the trickery you'll use is a mask to darken cells as they move away from the featured view and lighten as they get closer to the top.

Go back to Main.storyboard and add a UIView on to the UIImageView you added previously.

Screen Shot 2015-05-18 at 12.15.33 AM

In the Pin auto layout menu, uncheck Constrain to Margins, and this time, set all four spaces to 0. Select Items of New Constraints for the Update Frames dropdown and apply the four new constraints.

Screen Shot 2015-05-18 at 12.17.11 AM

Next, hook up the imageCoverView outlet from InspirationCell to the UIView you just added. Select the view again, and in Attributes Inspector set the Background Color to Black Color.

Now that you've set up the UI to mask the images, it's time to update the mask in code.

Go to InspirationCell.swift in the Views folder, and at the bottom of the file, add the following method:

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
  // 1
  let standardHeight = UltravisualLayoutConstants.Cell.standardHeight
  let featuredHeight = UltravisualLayoutConstants.Cell.featuredHeight

  // 2
  let delta = 1 - ((featuredHeight - CGRectGetHeight(frame)) / (featuredHeight - standardHeight))
  // 3
  let minAlpha: CGFloat = 0.3
  let maxAlpha: CGFloat = 0.75
  imageCoverView.alpha = maxAlpha - (delta * (maxAlpha - minAlpha))

This method updates the effects applied to the cell as it's rendering and moving up or down the screen. Here's what's happening step-by-step:

  1. These are the two convenience height constants you've used previously.
  2. Calculate the delta of the cell as it's moving to figure out how much to adjust the alpha in the following step.
  3. Based on the range constants, update the cell's alpha based on the delta value.

The delta is calculated as a range of 0 to 1 as it determines the percentage of the height change from the standard height to the featured height cells. From there, the alpha can be calculated as the percentage delta is applied to the acceptable alpha range, which is between 0.3 and 0.75 in this case.

Build and run!


Adding Session Details

At this point, you've got the images looking great with a subtle parallax effect and alpha transparency, and you're probably feeling about ready to take a victory lap around your workstation.

I won't stop you, but before you get too excited, keep in mind that there's just one problem; without the context of the session talk, time and room, the images are little more than pretty pictures!

You'll add those bits next.

Note: This next section is directly from the provided answer to the challenge section of the video series. If you want to try and solve the problem yourself, stop here and read the first page in the Hands-on Challenge from the video post. If you've followed the written tutorial thus far, you're in the same place as where the video tutorial leaves you.

First, add the session title to each item. Open Main.storyboard and drag a UILabel from the Object Library onto the InspirationCell in the Document Outline. Make sure the label is created as a sibling, meaning it's on the same level in the hierarchy, of Image View and View.


Select the label, and in the Attributes Inspector make the following changes:

  • Set Text to “Inspiration”
  • Set Color to White
  • Set Font to Custom, Family to Avenir Next, with a Style of Demi Bold and a Size of 38
  • Set Alignment to Center


With the label still selected, click the Pin button in the bottom right-hand corner of Interface Builder. In the pop-up, make sure Constrain to margins is unchecked, and then add the following layout constraints:

  • Select Leading Space and set the constant to 0
  • Select Trailing Space and set the constant to 0
  • Set Update Frames to Items of New Constraints
  • Click Add 2 Constraints

Screen Shot 2015-05-22 at 11.46.38 PM

Finally, click the Align button, select Vertical Center in Container, change Update Frames to Items of New Constraints and click Add 1 Constraint:

Screen Shot 2015-05-17 at 11.03.33 PM

Your cell should now look like this:

Screen Shot 2015-05-22 at 11.47.04 PM

Next, you'll have to hook up the new label to the cell so you can set the text correctly. Open InspirationCell.swift and add a new property at the top of the file:

@IBOutlet private weak var titleLabel: UILabel!

Then, inside of the if block of didSet observer for inspiration add:

titleLabel.text = inspiration.title

This ensures the text of the label updates each time the value of the property changes.

Jump back to Main.storyboard and right-click on InspirationCell in the Document Outline to show the connections popup. Drag from titleLabel to the label you just created to connect the two:


Build and run, then check out those sweet, sweet session titles!

Screen Shot 2015-05-22 at 11.47.27 PM

You know what would be a cool effect on the title labels? Having them scale as the user scrolls!

Cell Scaling

Open InspirationCell.swift and add the following to the bottom of applyLayoutAttributes(_:):

let scale = max(delta, 0.5)
titleLabel.transform = CGAffineTransformMakeScale(scale, scale)

Here you create a constant that’s the greater of either delta (which, if you remember, is a value between 0 and 1) and 0.5 -- this is important because you don’t want the label to scale to less than half its full size.

Next you use CGAffineTransformMakeScale(_:_:) to create a scaled transform, and set it on the label via its transform property.

Build and run. You’ll see that the session titles in the standard cells are half the size of the title in the featured cell, and that the label smoothly scales as a standard cell transitions into being the featured cell:

Screen Shot 2015-05-22 at 11.47.39 PM

Now it’s time to add the remainder of the session details to the cell and make them fade into view as the user scrolls.

Adding Session Details

Open Main.storyboard and drag two more Labels from the Object Library onto the cell in the Document Outline – again, make sure they’re created as siblings of the other views and not as children.

The updated Document Outline should look like the following:

Screen Shot 2015-05-22 at 11.47.50 PM

Using the Attributes Inspector, make the following changes to both of the new labels:

  • Set Text of one label to "Time, Room" and the other to "Speaker"
  • Set Color to White
  • Set Font to Custom, Family to Avenir Next, with a Style of Medium and a Size of 17
  • Set Alignment to Center

In the storyboard canvas, drag both labels down so that they appear below the large Inspiration label.

Position the Time, Room label below the Inspirations label. Put the Speaker label below the Time, Room label so that they are appear stacked atop one another.

Select the Time, Room label and click the Pin button. Uncheck Constrain to margins and then add the following layout constraints:

  • Select Leading and set the constant to 0
  • Enable Top and set the constraint to 0 -- make sure it’s referencing Inspiration and not InspirationCell by clicking on the dropdown arrow
  • Enable Trailing and set the constant to 0
  • Make sure Update Frames is set to Items of New Constraints
  • Click Add 4 Constraints

Now select the Speaker label and add the same layout constraints, but this time make sure the Top space is referencing Time, Room and not Inspiration or InspirationCell.

If you have difficulty getting the right value to come up in the dropdown, try nudging the label further down the view a bit, say, 10px, and look at the dropdown again. The view has to be sufficiently spaced so that Interface Builder can accurately detect what you're trying to do.

Your cell should now look like the following:

Screen Shot 2015-05-22 at 11.47.57 PM

Jump back to InspirationCell.swift and add the following outlets, just below the others:

@IBOutlet private weak var timeAndRoomLabel: UILabel! 
@IBOutlet private weak var speakerLabel: UILabel!

These will be used to set the Time, Room, and Speaker details on their corresponding labels.

Now, add the following to the bottom of the if block inside the didSet observer for the inspiration property:

timeAndRoomLabel.text = inspiration.roomAndTime 
speakerLabel.text = inspiration.speaker

Just like the titleLabel you set up previously, this ensures the values update whenever the inspiration values change.

Jump back to Main.storyboard and right-click on InspirationCell in the Document Outline to invoke the connections popup.

Drag from timeAndRoomLabel to the top label to connect them, and then drag from speakerLabel to the bottom label to connect those two.

Build and run. You’ll see that the session details now display as expected, but they don’t look quite right in the standard cells:

Screen Shot 2015-05-22 at 11.48.04 PM

Time to fix that!

Find applyLayoutAttributes(_:) in InspirationCell.swift and add the following to the very bottom:

timeAndRoomLabel.alpha = delta 
speakerLabel.alpha = delta

You’re simply setting the alpha of each label to the delta variable, which, if you remember, represents the progress of the cell transitioning to be the featured cell.

By doing this, you’re making sure the session details aren’t visible in standard cells, but fade in as each cell becomes the featured cell.

Once again, build and run. You’ll now see that session details aren’t displayed on the standard cells but instead fade in as you scroll:

Screen Shot 2015-05-22 at 11.48.13 PM

What a thing of beauty! :]

Smooth Scrolling

There's one last thing you have to do to make this app's UI really pop; you're going to tweak the way the collection view scrolls so that one cell is always in full focus at the top of the screen. This will help your users focus on the content, rather than spending energy on scrolling to an exact position.

Open UltravisualLayout.swift and add the following method to the class:

override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
  let itemIndex = round(proposedContentOffset.y / dragOffset)
  let yOffset = itemIndex * dragOffset
  return CGPoint(x: 0, y: yOffset)

This is a little-used method of UIScrollView that allows your app to respond with an effect similar to the page snapping effect of a paged UIScrollView.

When the user lifts their finger after a scroll while there's still some scroll velocity, this method will look into the future and tell you exactly where the scroll will end thanks to the proposedContentOffset. By returning a different scroll point, you can make the collection view end up right on an even boundary with a featured cell.

All you're doing in the implementation is finding the closest item to the proposed content offset, and then returning a CGPoint that's positioned so that item will be right against the top of the screen.

By default, a scroll view's deceleration rate is rather slow, which can make your target offset change look a bit strange. That's an easy fix though! Jump to InspirationsViewController.swift, and in viewDidLoad() add the following code to the end of the method:

collectionView!.decelerationRate = UIScrollViewDecelerationRateFast

Build, run and you're done. :]


Challenge: Tap to Select

Oh, there's one more thing. It's pretty cool to be able to scroll that list around, but did you notice what happens when you tap a cell?

That's right, nothing. #boring.

Why not scroll a tapped cell to the top so it gains focus? Take a shot at adding a new function to handle tapping events. Think about how you would handle selecting a row on a UITableView for a similar treatment.

In InspirationViewController.swift, make the cells handle the tap events by adding the following method to the class:

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
  let layout = collectionViewLayout as! UltravisualLayout
  let offset = layout.dragOffset * CGFloat(indexPath.item)
  if collectionView.contentOffset.y != offset {
    collectionView.setContentOffset(CGPoint(x: 0, y: offset), animated: true)


How close was your own code to the example solution?

Where to Go From Here

You can download the final project with all the code from this tutorial to check out the finished app.

In this tutorial, you added several auto layout-based labels, subtle parallax effects, and fading transitions to a standard Collection View.

Keeping class responsibilities focused -- especially for the view controllers -- is one of the main challenges in iOS development, and you handled them like a boss in this tutorial.

Now that you’ve covered the basics of adding a fancy expanding cell to UICollectionView, try experimenting with 3D transforms or fiddling with the scaling values in the collection layout to see what other effects you can achieve.

As mentioned, this tutorial is based on our Custom Collection View Layout video tutorial series. There are four custom collection view layouts in that series and this was just one of them — check out the rest of the videos for even more collection view fun!

If you have any questions about this tutorial, please join in the forum discussion below!