You are currently viewing Handling Cell Interactions When Using UIHostingConfiguration in iOS 16

Handling Cell Interactions When Using UIHostingConfiguration in iOS 16

UIHostingConfiguration is one of the tools introduced in iOS 16 to help developers in adding SwiftUI views into their UIKit apps. By using UIHostingConfiguration, developers can easily create a custom cell using SwiftUI and render it in a UITableView or UICollectionView.

In my previous article, I discussed in depth how to use UIHostingConfiguration to define the layout and content of a custom cell. In this article, let’s take things one step further and take a look at the user interaction aspect of it. Here’s what you will learn:

  • Handling cell’s selection state change
  • Handling cell’s swipe actions

So, without wasting much time, let’s dive into the details.


The Sample App

As usual, I will use a sample app to showcase the topics I am going to cover. Here’s what we are going to build:

The sample app

To get things started, I have pre-built the list above (without any of the user interactions handling) using UIHostingConfiguration. Feel free to use the code below to help you get started and follow along with this tutorial.

// MARK: - The custom cell
struct UserInteractionCell: View {

    var item: SFSymbolItem

    var body: some View {

        HStack(alignment: .center, spacing: 8) {
            Image(systemName: item.name)
            Text(item.name)
            Spacer()
        }
    }
}

// MARK: - The implementation
class UserInteractionViewController: UIViewController {

    var collectionView: UICollectionView!

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

    private var userInteractionCellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, SFSymbolItem>!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create cell registration
        userInteractionCellRegistration = .init { [unowned self] cell, indexPath, item in
            cell.contentConfiguration = createHostingConfiguration(for: item)
        }

        // 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
    }

    /// Create a content configuration that host the `UserInteractionCell`
    private func createHostingConfiguration(for item: SFSymbolItem) -> UIHostingConfiguration<UserInteractionCell, EmptyView> {
        return UIHostingConfiguration {
            // Create SwiftUI view
            UserInteractionCell(item: item)
        }
    }
}

extension UserInteractionViewController: 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]
        let cell = collectionView.dequeueConfiguredReusableCell(using: userInteractionCellRegistration,
                                                                for: indexPath,
                                                                item: item)
        return cell
    }
}

If you are having difficulties understanding the code above, I highly encourage you to first read my previous blog post called “How To Create Custom UICollectionViewListCell Using SwiftUI”.


Handling Cell’s Selection State Change

Before getting into state change handling, we must first give our UserInteractionCell a variable that represents its state. After that, we can set the cell’s styling based on the state variable we just defined.

struct UserInteractionCell: View {

    var item: SFSymbolItem
    
    /// Variable that represents the view state
    var state: UICellConfigurationState

    var body: some View {

        HStack(alignment: .center, spacing: 8) {

            Image(systemName: item.name)
                // Change SFSymbol styling based on state
                .font(state.isSelected ? .title2 : .body)
                .foregroundColor(state.isSelected ? Color(.systemRed) : Color(.label))

            Text(item.name)
                // Change text styling based on state
                .font(Font.headline.weight(state.isSelected ? .bold : .regular))
                .foregroundColor(state.isSelected ? Color(.systemRed) : Color(.label))

            Spacer()
        }
    }
}

In order to get notified when the cell’s state changes, we can use the cell’s configurationUpdateHandler introduced in iOS 15. The idea is to update the cell’s content configuration with a new UIHostingConfiguration instance every time the cell’s state changes.

cell.configurationUpdateHandler = { [unowned self] cell, state in
    // Create a new `UIHostingConfiguration` based on the state
    cell.contentConfiguration = createHostingConfiguration(for: item, with: state)
}

Notice that I have updated the createHostingConfiguration(for:) method to be able to accept the state variable so that we can use it to initialize the UserInteractionCell.

private func createHostingConfiguration(
    for item: SFSymbolItem,
    with state: UICellConfigurationState
) -> UIHostingConfiguration<some View, EmptyView> {
    
    return UIHostingConfiguration {
        // Use `state` to initialize `UserInteractionCell`
        UserInteractionCell(item: item, state: state)
    }
}

Do keep in mind that the configurationUpdateHandler is mainly used for updating the cell’s UI and layout when its state changes. Cell interactions such as tap handling will still be handled by the collection view or table view. In order words, use the usual collectionView(_:didSelectItemAt:) delegate method to handle tap actions from the users.

Disabling the Cell’s Default Selection Style

If you go ahead and tap on one of the cells, you will notice that the selected cell is highlighted in gray.

Disabling the UICollectionViewListCell default selection style
Selected cell is highlighted in gray

This is the default behavior that comes along with UICollectionViewListCell. For the sake of demonstration, let’s try to disable this behavior.

The idea is pretty simple, all we need to do is to set the cell’s background color to .systemBackground regardless of the state of the cell. For that, we can leverage the UIBackgroundConfiguration introduced in iOS 15.

cell.configurationUpdateHandler = { [unowned self] cell, state in
    
    // ...
    // ...

    // Set the cell's background configuration
    var newBgConfiguration = UIBackgroundConfiguration.listGroupedCell()
    newBgConfiguration.backgroundColor = .systemBackground
    cell.backgroundConfiguration = newBgConfiguration
}

That’s it for handling selection state change. Let’s move on!


Handling Cell’s Swipe Actions

The way to handle swipe actions on a cell created using UIHostingConfiguration is the same as handling swipe actions on a SwiftUI list row — by using the swipeActions modifier.

Here’s how to add a “trash” button at the trailing edge of UserInteractionCell:

UserInteractionCell(item: item, state: state)
    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
        Button(role: .destructive) { [unowned self] in
            showAlert(title: "Delete", message: item.name)
        } label: {
            Label("", systemImage: "trash")
        }
    }

The code above is pretty self-explanatory. However, do notice that we need to manually disable the cell’s full swipe ability as it is enabled by default. Also notice that by setting the button’s role to .destructive will result in a red color action button.

Next up, let’s try to add multiple buttons at the cell’s leading edge. As you might have guessed, all we need to do is to wrap 2 buttons within the swipeActions‘s content closure.

.swipeActions(edge: .leading) {

    Button("SHARE") { [unowned self] in
        showAlert(title: "Share", message: item.name)
    }
    .tint(.green)

    Button { [unowned self] in
        showAlert(title: "Favorite", message: item.name)
    } label: {
        Label("", systemImage: "star")
    }
    .tint(.yellow)
}

For the sample code above, there are 2 things that are noteworthy.

The first is that we can display text (instead of an icon) on an action button. This can be easily done by giving the button a title. Second is that the action button’s background color can be easily changed by using the tint modifier.


Wrapping Up

Except for UIHostingConfiguration, most of the things presented in this article are not new in iOS 16. If you have been using SwiftUI for quite some time, they should look fairly familiar to you.

On the other hand, if you are still using UIKit in your day-to-day development work, but would like to start adopting SwiftUI into your development routine, UIHostingConfiguration will definitely be a great starting point.

You can get the full sample code here.


I hope you enjoy reading this article, if you do, feel free to follow me on Twitter and subscribe to my monthly newsletter so that you won’t miss out on any of my upcoming iOS development-related articles.

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.