You are currently viewing How to Refresh Cell’s Content When Using UIHostingConfiguration

How to Refresh Cell’s Content When Using UIHostingConfiguration

When it comes to working with a table view or collection view, it is essential to ensure that the data model is always in sync with what is being shown on screen. Manually making sure that everything is in sync has always been the case when building UI using a table view or collection view.

With the introduction of UIHostingConfiguration in iOS 16, we can finally leverage the power of two-way binding that only exists in SwiftUI. With two-way binding, iOS will take care of the heavy lifting work of syncing the data model and the cell’s content for us. This means that we no longer need to call reloadItems(at:) to refresh a cell or call reconfigureItems(at:) to update the data source snapshot.

These are some very exciting improvements on UIKit that you do not want to miss, so let’s get right into it!


The Sample App

In order to showcase the greatness of two-way binding, let’s create a simple to-do list app as shown below:

The sample app

Before getting into the main topic, let’s define the model object that we can use to represent a to-do list entry.

class TodoItem {
    let title: String
    var completed: Bool

    init(_ title: String) {
        self.title = title
        self.completed = false
    }
}

On top of that, I have put together a simple list created using UIHostingConfiguration which you can use as a starter project for this article.

import SwiftUI

struct TodoListCell: View {

    var item: TodoItem

    var body: some View {
        HStack {
            Text(item.title)
                .font(Font.title2)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
}

class TodoListViewController: UIViewController {

    var collectionView: UICollectionView!
    private var todoListCellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, TodoItem>!

    let todoItems = [
        TodoItem("Write a blog post"),
        TodoItem("Call John"),
        TodoItem("Make doctor's appointment"),
        TodoItem("Reply emails"),
        TodoItem("Buy Lego for Jimmy"),
        TodoItem("Get a hair cut"),
        TodoItem("Book flight to Japan"),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create cell registration
        todoListCellRegistration = .init { cell, indexPath, item in

            cell.contentConfiguration = UIHostingConfiguration {
                TodoListCell(item: item)
            }

            var newBgConfiguration = UIBackgroundConfiguration.listGroupedCell()
            newBgConfiguration.backgroundColor = .systemBackground
            cell.backgroundConfiguration = newBgConfiguration
        }

        // 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
        
        // Add "Complete All" button
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Complete All",
                                                            style: .plain,
                                                            target: self,
                                                            action: #selector(completeAllTapped))
    }
    
    @objc private func completeAllTapped() {

    }
}

extension TodoListViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return todoItems.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let item = todoItems[indexPath.row]
        let cell = collectionView.dequeueConfiguredReusableCell(
            using: todoListCellRegistration,
            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”.


The Two-way Binding

Two-way binding is a fairly common yet extremely useful feature in SwiftUI. In SwiftUI, two-way binding is usually established between the model objects and the views. The term “two-way” indicates that any changes on the model objects will automatically trigger updates on the views, and any changes on the views will trigger updates on the model objects.

The fact that our to-do list app is using UIhostingConfiguration, we can forget about the traditional way of reloading cells, and take advantage of this awesome SwiftUI feature to keep our cell’s content always updated.

Here’s how it works:

Two-way binding when using UIHostingConfiguration in iOS 16
Two-way binding

Refreshing Cells With Two-way Binding

As shown in the image above, we want to establish a two-way binding between the TodoItem and the TodoListCell. Let’s first focus on binding the TodoItem to the TodoListCell.

Binding 1: Model Object to View

The first thing we need to do is to conform TodoItem to the ObservableObject. After that, add the @Published property wrapper to the completed property. This property wrapper is what triggers SwiftUI to update our cells when there are changes on completed.

class TodoItem: ObservableObject {

    let title: String
    @Published var completed: Bool

    init(_ title: String) {
        self.title = title
        self.completed = false
    }
}

After that, head over to TodoListCell and mark the item property as @ObservedObject. While we are here, let’s add some styling to the Text view based on the item‘s completed state.

struct TodoListCell: View {

    // Mark item as @ObservedObject
    @ObservedObject var item: TodoItem

    var body: some View {
        HStack {
            Text(item.title)
                .font(Font.title2)
                .frame(maxWidth: .infinity, alignment: .leading)
                // Change text color and strikethrough based on completed state
                .strikethrough(item.completed)
                .foregroundColor(item.completed ? .gray : .black)
        }
    }
}

With that, we have successfully bind the TodoItem to the TodoListCell. To see the binding in action, let’s implement the “Complete All” functionality by changing all the TodoItem‘s completed state to true.

@objc private func completeAllTapped() {
    // Update all elements to `completed = true`
    for item in todoItems {
        item.completed = true
    }
}

Now, if you run the sample code and tap on the “Complete All” button, you should see all the cell’s title changed to gray color with strikethrough applied.

Notice that we do not need to manually refresh each cell one by one. SwiftUI will automatically refresh the corresponding cell every time item (the observed object) changes. That’s the power of binding!

Binding 2: View to Model Object

The other part of the puzzle is to create a binding from the TodoListCell‘s UI element back to the TodoItem. To make that happen, we can use the Toggle view in SwiftUI which has a built-in binding functionality, and all we need to do is to use the $ prefix to establish the binding.

As a cherry on top, I also added animation() to the binding so that we can get a smooth strikethrough and color-changing animation every time when the completed state changes.

struct TodoListCell: View {

    @ObservedObject var item: TodoItem

    var body: some View {
        HStack {
            Text(item.title)
                .font(Font.title2)
                .frame(maxWidth: .infinity, alignment: .leading)
                .strikethrough(item.completed)
                .foregroundColor(item.completed ? .gray : .black)

            // Bind `Toggle` to `item`
            Toggle("", isOn: $item.completed.animation())
                .frame(maxWidth: 50)
        }
    }
}

With all that in place, we have successfully created a two-way binding between the TodoListCell and the TodoItem. As a result, every time when we tap on the Toggle view, the TodoItem‘s completed state will be updated, hence triggering a refresh to the corresponding TodoListCell.

Want to try out the sample code? Feel free to get it here.


The Good and the Bad

Now that you have seen two-way binding in action, you should notice that everything happens automatically. As long as we set up the bindings correctly, our model objects and cell’s content will always be in sync.

On top of that, thanks to UIHostingConfiguration, we can now easily harness the power of SwiftUI and use it in a UIKit environment. As you have seen earlier, animations and effects (such as strikethrough) that are somewhat troublesome to implement in UIKit have become extremely easy to achieve.

With all that being said, there’s a caveat that you should be aware of. The model object must be of reference type in this case, or else it will not be able to conform to the ObservableObject protocol. In most cases, this should not be a big deal, but I believe it is worth mentioning nevertheless.


Do you enjoy reading this article? If you do, I am sure you will also like my previous article “Handling Cell Interactions When Using UIHostingConfiguration in iOS 16“.

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.