You are currently viewing Building a List with UICollectionView in Swift

Building a List with UICollectionView in Swift

Since the introduction of UICollectionView in iOS6, UICollectionView has been the default component to go to when it comes to building a grid layout. In WWDC 2020, Apple pushes the usability of UICollectionView to the next level by introducing the list layout in UICollectionView.

By using the new features and APIs given to UICollectionView in iOS14, it is now super easy to build a UITableView-like list, making it arguably the better option when compared to UITableView.

In this article, let’s look at how simple it is to create a list with a swipeable cell that displays image and text using the new UICollectionView in iOS 14.


The Sample App

Before we get started, let’s take a quick look on what kind of list we are trying to build in this article. 

Building a List with UICollectionView in Swift
The sample app we going to build

As you can see from the above animated gif, we will create an app that displays a list of SFSymbol where when you tap on it, it will show the SFSymbol’s name in an UIAlertController.

Furthermore, the cells of the list are also swipeable and will trigger its corresponding swipe action when tapped.

With all that being said, let’s fire up your Xcode and dive right into it!


Creating a List

In order to create a list using UICollectionView, we need to:

  1. Define item identifier type
  2. Define section identifier type
  3. Create a collection view with list layout
  4. Define how data is shown using cell registration
  5. Define the collection view’s data source
  6. Create and apply a snapshot to the data source

1. Define Item Identifier Type

Before we start working on the collection view, we must first get ready with our data model. Let’s go ahead and create a SFSymbolItem struct which consists of a name and image constant.

struct SFSymbolItem: Hashable {
    let name: String
    let image: UIImage
    
    init(name: String) {
        self.name = name
        self.image = UIImage(systemName: name)!
    }
}

Make sure to conform the SFSymbolItem struct to the Hashable protocol because the diffable data source (introduced in iOS13) that we are going to use requires unique hash values of the item identifiers.

After that, let’s populate an array of SFSymbolItem so that later we can use it as the data model of our collection view.

let dataItems = [
    SFSymbolItem(name: "mic"),
    SFSymbolItem(name: "mic.fill"),
    SFSymbolItem(name: "message"),
    SFSymbolItem(name: "message.fill"),
    SFSymbolItem(name: "sun.min"),
    SFSymbolItem(name: "sun.min.fill"),
    SFSymbolItem(name: "sunset"),
    SFSymbolItem(name: "sunset.fill"),
    SFSymbolItem(name: "pencil"),
    SFSymbolItem(name: "pencil.circle"),
    SFSymbolItem(name: "highlighter"),
    SFSymbolItem(name: "pencil.and.outline"),
    SFSymbolItem(name: "personalhotspot"),
    SFSymbolItem(name: "network"),
    SFSymbolItem(name: "icloud"),
    SFSymbolItem(name: "icloud.fill"),
    SFSymbolItem(name: "car"),
    SFSymbolItem(name: "car.fill"),
    SFSymbolItem(name: "bus"),
    SFSymbolItem(name: "bus.fill"),
    SFSymbolItem(name: "flame"),
    SFSymbolItem(name: "flame.fill"),
    SFSymbolItem(name: "bolt"),
    SFSymbolItem(name: "bolt.fill")
]

2. Define Section Identifier Type

Even though there is only 1 section in our sample app, defining a section is still required. The common approach to define a section identifier is by using an enum.

Usually, the section identifier will only be used in 1 particular view controller, thus it is advisable to define it within the view controller that it is being used.

class BasicListViewController: UIViewController {
    
    // Define section identifier type
    enum Section {
        case main
    }

    // ...
    // ...
}

3. Create a Collection View with List Layout

With both item and section identifier in place, we are now ready to work on the collection view. First, let’s define a UICollectionView instance variable.

var collectionView: UICollectionView!

After that, head over to your view controller’s viewDidLoad() and create a collection view with a list-style layout. 

// Create list layout
let layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)

// Create collection view with list layout
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: listLayout)
view.addSubview(collectionView)

// Make collection view take up the entire view
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    collectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 0.0),
    collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
    collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
    collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])

One thing to note from the above sample code is that you will need to choose the collection view list appearance when creating a list configuration.

In iOS 14, Apple introduced 5 types of appearances that mimicked the appearance of a UITableView. Here are the screenshots of our sample app in different types of appearances.

UICollectionView list appearance (grouped, insetGrouped, plain)
Collection view list appearance (grouped, insetGrouped, plain)
UICollectionView list appearance (sidebar, sidebarPlain)
Collection view list appearance (sidebar, sidebarPlain)

4. Define How Data Is Shown Using Cell Registration

Next up, we will use the new UICollectionView.CellRegistration API introduced in iOS 14 to create a cell registration that defines how data should be shown in a cell.

// Create cell registration that defines how data should be shown in a cell
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SFSymbolItem> { (cell, indexPath, item) in
    
    // Define how data should be shown using content configuration
    var content = cell.defaultContentConfiguration()
    content.image = item.image
    content.text = item.name
    
    // Assign content configuration to cell
    cell.contentConfiguration = content
}

First, we create a cell registration for cells of type UICollectionViewListCell and data items of type SFSymbolItem.

Inside the cell registration handler, we create a default cell content configuration and use it to specify how we want the data (content) to be shown. After that, we will assign the content configuration to the cell.

5. Define the Collection View’s Data Source

With the cell registration in place, we can use it to define the data source of our collection view.

First, let’s define a UICollectionViewDiffableDataSource instance variable with Section as the section identifier type and SFSymbolItem as the item identifier type.

var dataSource: UICollectionViewDiffableDataSource<Section, SFSymbolItem>!

Next, head back to viewDidLoad() and create a data source by passing in our collection view and implementing a cell provider closure. Doing this will connect the data source with our collection view.

dataSource = UICollectionViewDiffableDataSource<Section, SFSymbolItem>(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, identifier: SFSymbolItem) -> UICollectionViewCell? in
    
    // Dequeue reusable cell using cell registration (Reuse identifier no longer needed)
    let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
                                                            for: indexPath,
                                                            item: identifier)
    // Configure cell appearance
    cell.accessories = [.disclosureIndicator()]
    
    return cell
}

Within the cell provider closure, we dequeue a reusable cell using the cell registration that we created previously. Note that we no longer need to use a cell reuse identifier in order to dequeue a reusable cell. How cool is that? 🥳

Furthermore, the cell provider closure is also a good place for you to configure the cell appearance such as adding cell accessories or changing the cell’s tint color.

6. Create and Apply a Snapshot to the Data Source

In this final step, we will tell our view controller what data to show by using the NSDiffableDataSourceSnapshot introduced in iOS 13.

As usual, let’s first create an NSDiffableDataSourceSnapshot instance variable. Make sure to set the section identifier type and item identifier type correctly.

var snapshot: NSDiffableDataSourceSnapshot<Section, SFSymbolItem>!

After that, head back to viewDidLoad() and add in the following code snippet.

// Create a snapshot that define the current state of data source's data
snapshot = NSDiffableDataSourceSnapshot<Section, SFSymbolItem>()
snapshot.appendSections([.main])
snapshot.appendItems(dataItems, toSection: .main)

// Display data in the collection view by applying the snapshot to data source
dataSource.apply(snapshot, animatingDifferences: false)

In the above code, we first create a snapshot instance, and then we let the snapshot know that it should contain the main section and the main section should contain dataItems we defined in step 1. After that, apply the snapshot to the data source to show the data on the collection view.

That’s it for creating a list using a collection view. Go ahead and run the sample code to see the list layout in action.


Handling Cell Tap Action

In this section, we will look into how to handle the cell tap action. As a recap, we will display the selected SFSymbol’s name in an UIAlertController as shown in the image below.

Handling cell tap action in UICollectionView with list layout
Show an alert after tap on cell

As you may have guessed, in order to handle the cell tap action, we will need to implement the collectionView(_:didSelectItemAt:) delegate method. But before that, make sure to set the view controller as the collection view’s delegate.

collectionView.delegate = self

Here’s the implementation of the collectionView(_:didSelectItemAt:) delegate method.

extension BasicListViewController: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView,
                        didSelectItemAt indexPath: IndexPath) {
        
        // Retrieve the item identifier using index path.
        // The item identifier we get will be the selected data item
        guard let selectedItem = dataSource.itemIdentifier(for: indexPath) else {
            collectionView.deselectItem(at: indexPath, animated: true)
            return
        }
        
        // Show selected SFSymbol's name
        let alert = UIAlertController(title: selectedItem.name,
                                      message: "",
                                      preferredStyle: .alert)
        
        let okAction = UIAlertAction(title:"OK", style: .default, handler: { (_) in
            // Deselect the selected cell
            collectionView.deselectItem(at: indexPath, animated: true)
        })
        alert.addAction(okAction)
        
        present(alert, animated: true, completion:nil)
    }
}

The above code is pretty much self explanatory. However, one thing to note is that we are retrieving the selected data directly from dataSource and not from dataItems.

This is very important because UICollectionViewDiffableDataSource might perform some background operation causing dataItems inconsistent with the data being displayed in the collection view.


Handling Swipe Action

In this section, we will look at how to enable and handle a cell’s swipe action.

Handling swipe action in UICollectionView with list layout
Actions when swipe cell from right to left

In order to enable swipe action on our list, let’s scroll back up to the beginning of viewDidLoad() and modify the layoutConfig by defining its trailingSwipeActionsConfigurationProvider. Make sure to change the definition of layoutConfig from let to var.

override func viewDidLoad() {
    super.viewDidLoad()
    
    // Create list layout
    var layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
    
    // Define right-to-left swipe action
    layoutConfig.trailingSwipeActionsConfigurationProvider = { [unowned self] (indexPath) in
        
        // Configure swipe action here...
        
    }
    
    //...
    //...
}

Pro tip:

Implement leadingSwipeActionsConfigurationProvider to enable the left-to-right swipe action.

Based on Apple documentation, the trailingSwipeActionsConfigurationProvider is a closure that accepts an IndexPath and returns a UISwipeActionsConfiguration.

The following sample code demonstrates how to implement the trailingSwipeActionsConfigurationProvider in order to get our sample app behavior.

// Define right-to-left swipe action
layoutConfig.trailingSwipeActionsConfigurationProvider = { [unowned self] (indexPath) in
    
    // 1
    guard let item = dataSource.itemIdentifier(for: indexPath) else {
        return nil
    }
    
    // 2
    // Create action 1
    let action1 = UIContextualAction(style: .normal, title: "Action 1") { (action, view, completion) in
        
        // 3
        // Handle swipe action by showing alert message
        handleSwipe(for: action, item: item)
        
        // 4
        // Trigger the action completion handler
        completion(true)
    }
    // 5
    action1.backgroundColor = .systemGreen
    
    // 6
    // Create action 2
    let action2 = UIContextualAction(style: .normal, title: "Action 2") { (action, view, completion) in
        
        // Handle swipe action by showing an alert message
        handleSwipe(for: action, item: item)
        
        // Trigger the action completion handler
        completion(true)
    }
    action2.backgroundColor = .systemPink
    
    // 7
    // Use all the actions to create a swipe action configuration
    // Return it to the swipe action configuration provider
    return UISwipeActionsConfiguration(actions: [action2, action1])
}

Inside the trailingSwipeActionsConfigurationProvider closure, we:

  1. Retrieve the swiped cell’s data item from the data source, just like what we did in collectionView(_:didSelectItemAt:).
  2. Define the first action (action 1) by creating an instance of UIContextualAction. During the initialization, we set the action’s style, title, and implement the action handler.
  3. Inside the action handler, call the handleSwipe(for:item:) function to show an alert message. We will be implementing this shortly.
  4. Call the action handler’s completion handler to indicate that we have performed the action.
  5. Configure the action view background color.
  6. Define the second action and implement its action handler.
  7. Create a swipe action configuration using all the UIContextualAction we just created and return it to the swipe action configuration provider.

Lastly, let’s implement the handleSwipe(for:item:) function.

private extension BasicListViewController {
    
    func handleSwipe(for action: UIContextualAction, item: SFSymbolItem) {
        
        let alert = UIAlertController(title: action.title,
                                      message: item.name,
                                      preferredStyle: .alert)
        
        let okAction = UIAlertAction(title:"OK", style: .default, handler: { (_) in })
        alert.addAction(okAction)
        
        present(alert, animated: true, completion:nil)
    }
}

With that, we have completed the sample app implementation. Go ahead and run your sample project to see everything in action.

I have uploaded the full sample project to GitHub, feel free to download it if you need any references.


Further Readings


Wrapping Up

The topics being covered in this article has barely scratched the surface of what you can do with a list created using UICollectionView. There are still other more advanced topics such as multi-section list, custom cell configuration, cell reordering, and expandable cells that we yet to explore.

If you would like to get notified when I publish new articles related to these topics, feel free to follow me on Twitter and subscribe to my monthly newsletter.

Thanks for reading. 🧑🏻‍💻


Related WWDC Videos


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.