Expanding Cells in iOS Collection Views

Learn how to make expanding cells in iOS collection views, as in the Ultravisual app. By Naeem Shaikh.

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

Scaling Cells

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

let scale = max(delta, 0.5)
titleLabel.transform = CGAffineTransform(scaleX: scale, y: 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 CGAffineTransform(scaleX:y:) to create a scaled transform that you set 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 labels smoothly scale as the standard cells transition into being the featured cell:

Session Titles

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

Using the Attributes inspector, set Text of one of the labels to "Time, Room" and the other to "Speaker". Then make the following changes to both of them:

  • Set Color to White Color.
  • 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 the Time, Room label below the Inspiration label. Then drag the Speaker label below the Time, Room label so that they appear stacked atop one another.

Select the Time, Room label and click the Add New Constraints button. Make sure Constrain to margins is unchecked and then add the following layout constraints:

  • Select Leading and set the constraint to 0.
  • Select Top and set the constraint to 0 — make sure it’s referencing the Bottom of Inspiration (Title Label) by clicking on the dropdown arrow.
  • Select Trailing and set the constant to 0.

Click Add 3 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, by 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:

Cell Design

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!

You'll use these 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 pop-up.

Drag from timeAndRoomLabel to the middle 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:

Session Titles with Details

Time to fix that!

In InspirationCell.swift, find apply(_:) 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 up:

Set Alpha

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 targetContentOffset(
  forProposedContentOffset 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 = .fast

Build, run and you're done!

FinalKit

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 that 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.

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

override func collectionView(_ collectionView: UICollectionView,
                             didSelectItemAt indexPath: IndexPath) {
  guard let layout = collectionViewLayout
    as? UltravisualLayout else {
      return
  }
  let offset = layout.dragOffset * CGFloat(indexPath.item)
  if collectionView.contentOffset.y != offset {
    collectionView.setContentOffset(
      CGPoint(x: 0, y: offset), animated: true
    )
  }
}

[/spoiler]

How close was your own code to the example solution?