iOS Tutorial: Want your Swift app to scroll in two directions, like Netflix? Here’s how.

Nest UICollectionView inside UITableView for independent scrolling

Want to give users a way to casually explore different categories of content without having to tap into a detail page? Independently scrolling rows can help you achieve this effect. Netflix and the App Store use this type of UI for their discovery dashboards.

netflix and the app store

Since the images are in sort of a grid, a Collection View seems like a natural tool of choice. Collection Views do support scrolling in both directions, but its sections are not designed to scroll independently. This is where Table Views come into the picture.

This tutorial covers how to leverage a Table View to handle vertical scrolling, while using a Collection View for the horizontal part. If you’re new to Collection Views, they’re very similar to Table Views in that they have a Data Source, Delegate, and reusable cells. Previously, we covered Table Views in this intro tutorial.

Update: There is now a Part 2 which covers how to make an API call to IMDB and populate the cells with movie images. It also integrates some of the questions raised in the comments, such as creating data models.

The Approach

view hierarchy

The Table View is going to handle vertical scrolling. Each genre (action, drama, etc) is a table section that contains one header and one cell. The key really is to nest a Collection View inside the Table View row. Just remember to point the Collection View DataSource to the table cell rather than the View Controller, and everything will be just fine.

Set Up the Table View

1. Create a new Single View Application using Swift. In Xcode, go to FileNewProject, and choose iOSApplicationSingle View Application.

2. In the Storyboard, drop a Table View into the View Controller, but try to line up the edges before you let go.

3. From the Resolve Auto Layout Issues icon popup, choose Add Missing Constraints. Xcode doesn’t always add the constraints you have in mind, but you can use this when you’re feeling lucky.

4. Control-drag from the Table View to the View Controller and add the DataSource outlet.

5. Within ViewController.swift, add the UITableViewDataSource protocol to the class declaration.  It should look something like this:

class ViewController: UIViewController, UITableViewDataSource {
}

6. Inside the ViewController class, add an array of the different video genres.

var categories = ["Action", "Drama", "Science Fiction", "Kids", "Horror"]

7. Implement numberOfSectionsInTableView so that there’s one section per genre.

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return categories.count
}

8. Implement titleForHeaderInSection so that the section header title is simply the genre name.

func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return categories[section]
}

9. Implement numberOfRowsInSection so that there’s only one row per section.

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 1
}

10. Implement cellForRowAtIndexPath. Use cell as the identifier and CategoryRow as the class.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! CategoryRow
    return cell
}

Note: the compiler will complain about CategoryRow. Don’t worry, you will create this momentarily.

Your ViewController.swift file should look something like this:

import UIKit

class ViewController: UIViewController, UITableViewDataSource {
    var categories = ["Action", "Drama", "Science Fiction", "Kids", "Horror"]
    
    func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return categories[section]
    }
    
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return categories.count
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! CategoryRow
        return cell
    }
    
}

11. Now to silence that compiler warning… Create a file (FileNewFile…) and choose iOSSource. Name the file CategoryRow and add the following code to subclass UITableViewCell:

import UIKit

class CategoryRow : UITableViewCell { }

12. One final loose end to tie up. Back in the Storyboard, add a prototype cell to the Collection View (edited) Table View.  Click on the cell and set the Identifier to cell. Also set the cell class to CategoryRow.

configuring the cell

13. Build and Run. You should see something like this:

empty genres in the table

Set Up the Collection View

The Table View Cell represents the videos within a single genre. Not only does it contain a Collection View, it also serves as its DataSource.

1. Back in the Storyboard, resize theTable View Cell to make it taller. Drop in a Collection View, and line up the constraints.

nest a collection view inside the table cell

2. The Collection View already comes with a prototype cell, so set that cell’s Identifier to videoCell.

set identifier to videoCell

3. Now for the tricky part — wiring the Collection View’s DataSource and Delegate to the cell. It’s easier if you drag from the Collection View’s Connections Inspector to the Table View Cell within the view hierarchy.

wire datasource to cell

4. To pretty things up, go to the Size Inspector of the Collection View and set the Min Spacing and Section Insets all to 5.

spacing and insets

5. On the Attributes Inspector of the Collection View, change the Scroll Direction to horizontal.

6. Switch to the Collection View Cell and set its background color to red.

7. Within CategoryRow.swift, implement the Collection View DataSource method numberOfItemsInSection. For now, hard-code 12 videos per genre.

extension CategoryRow : UICollectionViewDataSource {
    
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 12
    }
    
}

8. Also implement cellForItemAtIndexPath and dequeue the cell using the videoCell identifier we set in the Storyboard earlier.

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("videoCell", forIndexPath: indexPath) as! UICollectionViewCell
    return cell
}

9. Almost done! For a Table View Cell’s height, you might be used to heightForRowAtIndexPath. Collection Views use a similar method sizeForItemAtIndexPath that determines both height and width. Drop in the code snippet below to fit a handful of cells on each row at a time.

extension CategoryRow : UICollectionViewDelegateFlowLayout {
    
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        let itemsPerRow:CGFloat = 4
        let hardCodedPadding:CGFloat = 5
        let itemWidth = (collectionView.bounds.width / itemsPerRow) - hardCodedPadding
        let itemHeight = collectionView.bounds.height - (2 * hardCodedPadding)
        return CGSize(width: itemWidth, height: itemHeight)
    }
    
}

The width is just the Collection View width divided by four, and the height is just the Collection View height. Don’t worry about the padding adjustments — they were tweaked using trial and error. If anyone remembers their algebra from high school, I’m open to suggestions for a legitimate algorithm!

Your CategoryRow.swift file should look like this:

import UIKit

class CategoryRow : UITableViewCell { }

extension CategoryRow : UICollectionViewDataSource {
    
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 12
    }
    
    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("videoCell", forIndexPath: indexPath) as! UICollectionViewCell
        return cell
    }
    
}

extension CategoryRow : UICollectionViewDelegateFlowLayout {
    
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        let itemsPerRow:CGFloat = 4
        let hardCodedPadding:CGFloat = 5
        let itemWidth = (collectionView.bounds.width / itemsPerRow) - hardCodedPadding
        let itemHeight = collectionView.bounds.height - (2 * hardCodedPadding)
        return CGSize(width: itemWidth, height: itemHeight)
    }
    
}

10. Build and run, and you should now have independently scrolling rows that look something like this:

animated gif of horizontal scroller

More Information

sample project of this tutorial is available on GitHub.

If you’re using Objective-C, here are some other tutorials on this topic:

Although Collection Views are a fundamental piece of UI, they can actually get super complicated once you start messing with the layout. If you have a subscription at Ray Wenderlich, you might find these video tutorial series to be really helpful:

Have any suggestions for future tutorial topics? Feel free to add your thoughts to the comments.

Like this post? Please share it below! Then follow us on Twitter @thorntech or join our mailing list for future updates.

Get insights on SFTP Gateway, cloud computing and more, in your inbox.

Get smarter about all things tech. Sign up now!

Scroll to Top