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.
At the end of this article, you will learn:
- How to use the
UIHostingConfiguration
- How to adjust the cell height
- How to adjust the separator inset
- 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:
- The ability to display cell accessories
- The ability to adjust the separator inset (more on that later)
- 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.
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:
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 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.
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.