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.
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.
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 article — SFSymbolItem
. 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.
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:
- 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.
- 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.
- 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
- Table and Collection View Cells Reload Improvements in iOS 15
- Designing Custom UICollectionViewListCell in Interface Builder
- Building an Expandable List Using UICollectionView: Part 1
- Building an Expandable List Using UICollectionView: Part 2
- Replicate the Expandable Date Picker Using UICollectionView List
Related WWDC Videos
- Lists in UICollectionView
- Advances in UICollectionView
- Modern cell configuration
- Advances in UI Data Sources
👋🏻 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.