You are currently viewing UICollectionView List with Custom Cell and Custom Configuration

UICollectionView List with Custom Cell and Custom Configuration

In iOS14, Apple introduced the list layout in UICollectionView, allowing developers to easily create a list using UICollectionViewListCell — A collection view cell that comes along with iOS14 which provides list features and default styling.

Even though UICollectionViewListCell is highly customisable, there are still situations where we might need to create our own custom cell in order to fit our app’s requirements.

In this article, let’s look at how we can create a custom cell and use it alongside a custom content configuration to build a list in a UICollectionView.

This is a continuation of my previous article “Building a List with UICollectionView in Swift“. Therefore, if you are not familiar with building a list using a collection view, I highly recommend you check it out before proceeding.


The Sample App

The following animated GIF showcases what we are trying to build in this article.

UICollectionView List with Custom Cell and Custom Configuration
The sample app we going to build

As you can see, all the cells in the list have a layout that is different from the usual side-by-side image-text layout. Instead, they all have a custom top-bottom image-text layout.

Furthermore, the appearance of the custom cells will change based on the state of the cell. When the cell is selected, its text will become red and the text’s weight will become heavy. The same goes to the symbol.


The Concept

Before we start implementing the custom cell, we must first understand what is content configuration, content view, and also the relationship between a custom cell, a content configuration, and a content view.

A content view is a UIView subclass that conforms to the UIContentView protocol. It defines the layout and appearance of the custom cell. It is also in charge of displaying the correct data and appearance based on the provided content configuration.

A content configuration will be the content view‘s view model and it is conforms to the UIContentConfiguration protocol. On top of that, it is also in charge of generating a content view instance for the custom cell. Thus, you can treat it as a bridge that links up both content view and custom cell.

A custom cell of a UICollectionView list is a subclass of UICollectionViewListCell. It has only 1 task — generate a properly configured content configuration object based on the state (selected, highlighted, disabled, etc.) of the cell and then assign the configuration to itself.

To summarize, the custom cell will create and assign a content configuration object to itself. The content configuration object will then generate a content view for the custom cell, and the content view will display the data provided by the content configuration object.

The custom UICollectionViewListCell concept overview
The custom cell concept overview

Don’t worry if all these sound a bit confusing to you, I am sure things will clear up once we start looking into the sample code.


The Data Item Type

For the sample app, we will reuse the data item type of my previous articleSFSymbolItem. As a recap, here’s the definition of SFSymbolItem.

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

Define Custom Content Configuration and Content View

Let’s begin building our list by defining the custom content configuration and content view.

We will call our custom content configuration SFSymbolContentConfiguration and make sure it is conform to both UIContentConfiguration and Hashable protocol.

struct SFSymbolContentConfiguration: UIContentConfiguration, Hashable {
    
    // We will work on the implementation in a short while.

}

Next up, create a UIView subclass called SFSymbolVerticalContentView and conform it to the UIContentView protocol. This will be the content view of our custom cell.

class SFSymbolVerticalContentView: UIView, UIContentView {
	
	// We will work on the implementation in a short while.
    
    init(configuration: SFSymbolContentConfiguration) {
       // Custom initializer implementation here.
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Custom Content Configuration Implementation

With the SFSymbolVerticalContentView class definition in place, we are now ready to implement SFSymbolContentConfiguration.

As I mentioned before, the content configuration will act as the view model of the content view. Thus, let’s start by defining all the properties that will be consumed by the content view.

struct SFSymbolContentConfiguration: UIContentConfiguration, Hashable {
    
    var name: String?
    var symbol: UIImage?
    var nameColor: UIColor?
    var symbolColor: UIColor?
    var symbolWeight: UIImage.SymbolWeight?
    var fontWeight: UIFont.Weight?

}

After that, we will conform to the UIContentConfiguration protocol by implementing its method requirements:

/// Initializes and returns a new instance of the content view using this configuration.
func makeContentView() -> UIView & UIContentView

/// Returns the configuration updated for the specified state, 
/// by applying the configuration's default values for that state to 
/// any properties that have not been customized.
func updated(for state: UIConfigurationState) -> Self

The content view instant return by the makeContentView() method will be used as the custom cell’s content view. Therefore, its implementation is pretty straightforward, we just need to return an instance of SFSymbolVerticalContentView.

func makeContentView() -> UIView & UIContentView {
    return SFSymbolVerticalContentView(configuration: self)
}

The 2nd method requirement — updated(for:) in charge of giving correct values to all the content configuration’s properties that are not related to the cell’s data items.

Furthermore, the value will be assigned based on a given state. For our case, we only care about isSelected state and state other than isSelected. Following is the implementation of the method.

func updated(for state: UIConfigurationState) -> Self {
    
    // Perform update on parameters that does not related to cell's data itesm
    
    // Make sure we are dealing with instance of UICellConfigurationState
    guard let state = state as? UICellConfigurationState else {
        return self
    }
    
    // Updater self based on the current state
    var updatedConfiguration = self
    if state.isSelected {
        // Selected state
        updatedConfiguration.nameColor = .systemPink
        updatedConfiguration.symbolColor = .systemPink
        updatedConfiguration.fontWeight = .heavy
        updatedConfiguration.symbolWeight = .heavy
    } else {
        // Other states
        updatedConfiguration.nameColor = .systemBlue
        updatedConfiguration.symbolColor = .systemBlue
        updatedConfiguration.fontWeight = .regular
        updatedConfiguration.symbolWeight = .regular
    }

    return updatedConfiguration
}

With that we have implemented a custom content configuration for our custom cell.


Custom Content View Implementation

In this section, let’s implement the custom content view class SFSymbolVerticalContentView that we defined not long ago.

class SFSymbolVerticalContentView: UIView, UIContentView {
	
	// We will work on the implementation in a short while.
    
    init(configuration: SFSymbolContentConfiguration) {
       // Custom initializer implementation here.
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Let’s begin the implementation by defining the required UI elements. For the custom cell, we will need an UILable to display the SFSymbol name and an UIImageView to show the symbol image.

class SFSymbolVerticalContentView: UIView, UIContentView {
    
    let nameLabel = UILabel()
    let symbolImageView = UIImageView()

    // ...
    // ...
}

After that, it is time to conform to the UIContentView protocol. By looking at the documentation, there’s only 1 property requirement to meet:

/// Returns the current configuration of the view.
/// Setting this property applies the new configuration to the view.
var configuration: UIContentConfiguration

Here’s how we make SFSymbolVerticalContentView satisfy the requirement:

class SFSymbolVerticalContentView: UIView, UIContentView {
    
    // ...
    // ...

    private var currentConfiguration: SFSymbolContentConfiguration!
    var configuration: UIContentConfiguration {
        get {
            currentConfiguration
        }
        set {
            // Make sure the given configuration is of type SFSymbolContentConfiguration
            guard let newConfiguration = newValue as? SFSymbolContentConfiguration else {
                return
            }
            
            // Apply the new configuration to SFSymbolVerticalContentView
            // also update currentConfiguration to newConfiguration
            apply(configuration: newConfiguration)
        }
    }

    // ...
    // ...
}

In the above code, we define a variable named currentConfiguration of type SFSymbolContentConfiguration to store the content configuration being assigned to the content view.

After that, we satisfy the UIContentView protocol requirement by defining configuration as a computed property, and use it to retrieve and set the value of currentConfiguration.

Do note that apply(configuration:) is a private function in charge of setting the value of currentConfiguration and applying all the currentConfiguration properties to the content view. We will implement this function in a bit.

Next up, we will work on the custom initializer init(configuration:) that we created during the definition of the SFSymbolVerticalContentView class.

We will perform 2 tasks during the initialization process — Setup the content view UI and apply the given content configuration to the content view.

init(configuration: SFSymbolContentConfiguration) {
    super.init(frame: .zero)
    
    // Create the content view UI
    setupAllViews()
    
    // Apply the configuration (set data to UI elements / define custom content view appearance)
    apply(configuration: configuration)
}

And here are the implementations of the setupAllViews() and apply(configuration:).

private extension SFSymbolVerticalContentView {
    
    private func setupAllViews() {
        
        // Add stack view to content view
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.alignment = .fill
        stackView.distribution = .fill
        addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
            stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
        ])
        
        // Add image view to stack view
        symbolImageView.contentMode = .scaleAspectFit
        stackView.addArrangedSubview(symbolImageView)
        
        // Add label to stack view
        nameLabel.textAlignment = .center
        stackView.addArrangedSubview(nameLabel)
    }
    
    private func apply(configuration: SFSymbolContentConfiguration) {
    
        // Only apply configuration if new configuration and current configuration are not the same
        guard currentConfiguration != configuration else {
            return
        }
        
        // Replace current configuration with new configuration
        currentConfiguration = configuration
        
        // Set data to UI elements
        nameLabel.text = configuration.name
        nameLabel.textColor = configuration.nameColor
        
        // Set font weight
        if let fontWeight = configuration.fontWeight {
            nameLabel.font = UIFont.systemFont(ofSize: nameLabel.font.pointSize,
                                               weight: fontWeight)
        }
        
        // Set symbol color & weight
        if
            let symbolColor = configuration.symbolColor,
            let symbolWeight = configuration.symbolWeight {
            
            let symbolConfig = UIImage.SymbolConfiguration(weight: symbolWeight)
            var symbol = configuration.symbol?.withConfiguration(symbolConfig)
            symbol = symbol?.withTintColor(symbolColor, renderingMode: .alwaysOriginal)
            symbolImageView.image = symbol
        }
    }
}

The implementations of both of these functions are pretty much self-explanatory. The setupAllViews() function uses auto layout to add an UIStackView containing UILabel and UIImageView into the content view. On the other hand, the apply(configuration:) function applies all the given content configuration properties to the content view’s UI elements.

Note:

Check out this article if you would like to learn how to design a custom UICollectionViewListCell using interface builder.


Custom Cell Implementation

The last part of the puzzle is to define a custom cell which is a subclass of UICollectionViewListCell so that we can display the content view inside an UICollectionView.

We will use the custom cell to keep track of the data item (SFSymbolItem) currently being displayed. Therefore, let’s create a custom cell class named SFSymbolVerticalListCell with a property named item of type SFSymbolItem.

class SFSymbolVerticalListCell: UICollectionViewListCell {
    
    var item: SFSymbolItem?

}

Prior to iOS14, a UICollectionView custom cell will need to take care of laying out cell UI, defining cell appearance, and displaying cell data. In iOS14, we have offset all of these tasks to the content configuration and content view.

Therefore, there’s only 1 task left that needs to be handled by our custom cell — Assign itself a content configuration object based on the cell’s current state and data item.

This can be easily achieved by overriding the updateConfiguration(using:) method of UICollectionViewListCell. This method will be triggered every time the state of a cell changes.

class SFSymbolVerticalListCell: UICollectionViewListCell {
    
    var item: SFSymbolItem?
    
    override func updateConfiguration(using state: UICellConfigurationState) {
            
        // Create new configuration object and update it base on state
        var newConfiguration = SFSymbolContentConfiguration().updated(for: state)
        
        // Update any configuration parameters related to data item
        newConfiguration.name = item?.name
        newConfiguration.symbol = item?.image

        // Set content configuration in order to update custom content view
        contentConfiguration = newConfiguration
        
    }
}

The code above is pretty straightforward, we generate a new content configuration object based on the cell’s current state, set the name and image to the object and assign it to the cell.

That’s it! We have finished implementing the custom cell.


Setting up Collection View

With all the 3 main components (custom cell, content view, and content configuration) in place, we are now ready to put everything together to see them in action.

The way of showing custom cells in a collection view is very similar to how we show the standard UICollectionViewListCell in a collection view. I won’t go in-depth into this as it has been covered in my previous article.

However, one thing to note is that we must register the SFSymbolVerticalListCell to the collection view. On top of that, within the cell registration handler, we only need to set the data item to the cell and let the cell’s updateConfiguration(using:) method to take care of assigning the content configuration object to the cell.

let cellRegistration = UICollectionView.CellRegistration<SFSymbolVerticalListCell, SFSymbolItem> { (cell, indexPath, item) in
    
    // For custom cell, we just need to assign the data item to the cell.
    // The custom cell's updateConfiguration(using:) method will assign the
    // content configuration to the cell
    cell.item = item
}

Here’s the full viewDidLoad() method of our view controller.

override func viewDidLoad() {
    super.viewDidLoad()

    // 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),
    ])
    
    // Create cell registration that define how data should be shown in a cell
    let cellRegistration = UICollectionView.CellRegistration<SFSymbolVerticalListCell, SFSymbolItem> { (cell, indexPath, item) in
        
        // For custom cell, we just need to assign the data item to the cell.
        // The custom cell's updateConfiguration(using:) method will assign the
        // content configuration to the cell
        cell.item = item
    }
    
    // Define data source
    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)
        
        return cell
    }
    
    // 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 on the collection view by applying the snapshot to data source
    dataSource.apply(snapshot, animatingDifferences: false)
}

Phew… We have written quite a lot of code! Let’s build and run the sample project to see everything in action.


Changing Cell’s Background Color

When you are playing around with the sample app, you should notice that the cell’s background will become gray when it is selected.

Changing background color of selected custom UICollectionViewListCell
Custom cell’s background color when selected

This is certainly not what we want! How should we change the cell’s background color when it is selected? Luckily Apple introduced a new class called UIBackgroundConfiguration that works similarly to the UIContentConfiguration.

With that in mind, let’s head back to the SFSymbolVerticalListCell‘s updateConfiguration(using:) method and set the cell background configuration accordingly.

class SFSymbolVerticalListCell: UICollectionViewListCell {
    
    var item: SFSymbolItem?
    
    override func updateConfiguration(using state: UICellConfigurationState) {
        
        // Create a new background configuration so that
        // the cell must always have systemBackground background color
        // This will remove the gray background when cell is selected
        var newBgConfiguration = UIBackgroundConfiguration.listGroupedCell()
        newBgConfiguration.backgroundColor = .systemBackground
        backgroundConfiguration = newBgConfiguration
            
        // ...
        // ... 
    }
}

If you would like to have more control over the UIBackgroundConfiguration‘s behavior, you can definitely create a custom background configuration class. However, that will be a story for another day.


Wrapping Up

Creating custom UICollectionViewListCell using custom content configuration is a fairly new concept in iOS development and you might find it a little bit confusing at first.

To sum up what we have learned in this article:

  1. The content configuration will act as the view model of the content view. Furthermore, It is also in charge of defining the cell’s appearance in various states.
  2. The content view takes care of the custom cell’s UI. It consumes all the content configuration properties and sets them to the respective UI elements.
  3. The custom cell in charge of setting all the data item properties to the content configuration object and assigning it to the cell’s contentConfiguration property.

Feel free to download the full sample project from Github if you need any references.


I will try to cover some other topics such as custom background configuration, multi-section list, and expandable cells in the near future.

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. 🧑🏻‍💻


Further Readings


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.