You are currently viewing How To Create Custom UICollectionViewListCell Using SwiftUI

How To Create Custom UICollectionViewListCell Using SwiftUI

Every time when I use a storyboard or XIB file to create a custom UICollectionView or UITableView cell layout, I always wonder wouldn’t it be great if I can use SwiftUI to define the layout? In this year’s WWDC (2022) Apple finally made that happen. 

In this article, we will explore what it takes to build the following list using a collection view and SwiftUI.

How To Create Custom UICollectionViewListCell Using SwiftUI
The sample app

At the end of this article, you will learn:

  1. How to use the UIHostingConfiguration
  2. How to adjust the cell height
  3. How to adjust the separator inset
  4. How to adjust the cell’s layout margin

There are quite a lot of topics to be covered here, so let’s get right into it.


Getting Ready

Before diving into the main topic, there are a few things that we need to get in place. First, import the SwiftUI module to your view controller class.

import SwiftUI

After that, define the following data types that will act as the data model of our list:

enum Section {
    case main
}

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

let dataModel = [
        SFSymbolItem(name: "applelogo"),
        SFSymbolItem(name: "iphone"),
        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: "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")
    ]

Next up, go ahead and configure our collection view to use a list layout configuration.

override func viewDidLoad() {
    super.viewDidLoad()

    // Configure collection view using list layout
    let layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
    let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
    collectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout)
    collectionView.dataSource = self
    view = collectionView
}

In the code above, notice that we are not using a diffable data source. This means that a diffable data source is not mandatory when creating a custom cell using SwiftUI. Furthermore, since the list that we are building is just showing a static data set, using a traditional data source is much more straightforward and easier to understand.

Lastly, let’s implement the required UICollectionViewDataSource methods:

extension SwiftUICustomCellViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dataModel.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let item = dataModel[indexPath.row]
        
        // Use `swiftUICellRegistration` to dequeue the custom SwiftUI cell
        let cell = collectionView.dequeueConfiguredReusableCell(using: swiftUICellRegistration, for: indexPath, item: item)
        
        return cell
    }
}

Notice the swiftUICellRegistration used in the code above, we will be working on that in just a moment. For now, just keep in mind that using the swiftUICellRegistration is how we link up our custom SwiftUI cell with the collection view.

With all that out of the way, we can now get into the fun stuff.


Using the UIHostingConfiguration

Prior to iOS 16, in order to create a custom UICollectionViewListCell, we need to create a subclass of UICollectionViewListCell and define a custom configuration object that conforms to the UIContentConfiguration protocol, which is somewhat troublesome.

With the introduction of UIHostingConfiguration in iOS 16, it is now possible to define the layout and content of a custom cell using SwiftUI, eliminating the need to create a UICollectionViewListCell subclass and a custom configuration object.

Based on Apple’s documentation, the UIHostingConfiguration struct is conformed to the UIContentConfiguration protocol. Therefore we can use it during cell registration like so:

private var swiftUICellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, SFSymbolItem> = {
    .init { cell, indexPath, item in
        
        let hostingConfiguration = UIHostingConfiguration {
            
            // Define SwiftUI view here
            // ...
            // ...
        }
         
        // Make hosting configuration as the cell's content configuration
        cell.contentConfiguration = hostingConfiguration
    }
}()

From the code above, there are 2 things that you need to be aware of.

First, the cell type that we use for cell registration is UICollectionViewListCell (not UICollectionViewCell). Since we are building a list, using UICollectionViewListCell will gain us some of its useful functionalities such as:

  1. The ability to display cell accessories
  2. The ability to adjust the separator inset (more on that later)
  3. Various cell appearances (based on the collection view’s layout configuration)

Second, notice that we are defining the cell registration as an instance variable (swiftUICellRegistration). As you have seen earlier, this enables us to use swiftUICellRegistration in the collection view’s data source method.

OK, enough said, let’s define the cell’s layout and content using SwiftUI so that we can see everything in action.

let hostingConfiguration = UIHostingConfiguration {
    
    // Define cell's content & layout using SwiftUI
    HStack(alignment: .firstTextBaseline) {
        Image(systemName: item.name)
            .padding()
        Spacer()
        Text(item.name)
            .font(.system(.title3, weight: .semibold))
        Spacer()
        Image(systemName: item.name)
            .padding()
    }
    .background {
        RoundedRectangle(cornerRadius: 12.0)
            .fill(Color(.systemYellow))
    }
}

At this point, if you try to run the sample code, you will see the following cells being populated.

Using UIHostingConfiguration to create custom cell layout
The result of using UIHostingConfiguration to create custom cell layout

Making the Adjustments

Adjusting the Cell Height

From the image above, you should notice that our current cell height is a bit smaller than what we are expecting. As mentioned in one of my previous articles, UICollectionViewListCell is a self-sizing cell. This means that it will automatically adjust its size based on its layout and content.

With that in mind, we can easily increase the cell height by simply increasing the height of the HStack.

HStack(alignment: .firstTextBaseline) {
    
    // ...
    // ...
}
.frame(height: 70) // Set HStack height to 70
.background {
    RoundedRectangle(cornerRadius: 12.0)
        .fill(Color(.systemYellow))
}

Note:

Make sure to set the HStack‘s frame before setting the yellow color background view, or else the yellow background view height will not increase accordingly.

Here’s the end result:

Adjust cell height when using UIHostingConfiguration
Cells with adjusted height

Adjusting the Separator Inset

Currently, the leading edge of the separator is aligned with the text in the cell. This is the default behavior inherited from the UICollectionViewListCell. Unfortunately, this is not what we want.

To achieve what we want, we can use the alignmentGuide modifier like so:

HStack(alignment: .firstTextBaseline) {
    
    // ...
    // ...
}
.frame(height: 70)
.background {
    RoundedRectangle(cornerRadius: 12.0)
        .fill(Color(.systemYellow))
}
// Align the separator with the HStack leading & trailing edge
.alignmentGuide(.listRowSeparatorLeading) { $0[.leading] }
.alignmentGuide(.listRowSeparatorTrailing) { $0[.trailing] }

What the above code did is align the separator leading edge with the HStack leading edge and align the separator trailing edge with the HStack trailing edge. Here’s what we will get:

Adjusting cell separator inset when using UIHostingConfiguration
Adjusted cell separator inset

Adjusting the Cell’s Layout Margins

By default, the cell’s SwiftUI content is inset from the edges of the cell, based on the cell’s layout margins in UIKit. In order to overwrite the default margin, we can use the UIHostingConfiguration‘s margins modifier to do so.

let hostingConfiguration = UIHostingConfiguration {
    
    // SwiftUI view here 
    // ...
    // ...
    // ...

}.margins(.horizontal, 50)

With that, our custom cell has reached its final form.

Adjusting cell content margins when using UIHostingConfiguration
Cell with adjusted content margins

One Final Bit…

Now that we have finished composing our custom cell using SwiftUI, we can perform a simple refactoring by creating a dedicated SwiftUI view for our custom cell layout.

struct MyFirstSwiftUICell: View {
    
    var item: SFSymbolItem
    
    var body: some View {
        
        HStack(alignment: .firstTextBaseline) {
            Image(systemName: item.name)
                .padding()
            Spacer()
            Text(item.name)
                .font(.system(.title3, weight: .semibold))
            Spacer()
            Image(systemName: item.name)
                .padding()
        }
        .frame(height: 70)
        .background {
            RoundedRectangle(cornerRadius: 12.0)
                .fill(Color(.systemYellow))
        }
        .alignmentGuide(.listRowSeparatorLeading) { $0[.leading] }
        .alignmentGuide(.listRowSeparatorTrailing) { $0[.trailing] }
        
    }
}

With that in place, we can now create the UIHostingConfiguration like so:

let hostingConfiguration = UIHostingConfiguration {
    MyFirstSwiftUICell(item: item)
}.margins(.horizontal, 50)

Notice how we configure the MyFirstSwiftUICell‘s content by passing in the item object during initialization.

With this simple refactoring, we have successfully improved the readability of our code when creating a UIHostingConfiguration. Furthermore, we also converted our custom cell’s layout into a reusable component.


Wrapping Up

The example I use throughout this article is mainly focused on creating a custom UICollectionViewListCell, but that doesn’t mean that you cannot use UIHostingConfiguration to create a custom UICollectionViewCell. In fact, according to Apple, UIHostingConfiguration is designed to be able to work on both UICollectionViewCell and UITableViewCell.

However, do notice that UIHostingConfiguration is only available in iOS 16 and above. If your app still needs to support iOS version lower than iOS 16, then you might want to consider fallbacking to the non-SwiftUI way.

Last but not least, here’s the full sample code.


I hope you enjoy reading this article, if you do, feel free to check out my other collection view related articles here. You can also follow me on Twitter, and subscribe to my monthly newsletter.

Thanks for reading. 👨🏻‍💻


👋🏻 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.