You are currently viewing Create Custom Cell with Placeholder UI Using UIHostingConfiguration

Create Custom Cell with Placeholder UI Using UIHostingConfiguration

Prior to iOS 16, having a collection view or table view that will display placeholder cells while waiting for data to load is somewhat troublesome to implement. We will have to define 2 separate custom cells (the placeholder cell and the actual cell) and manually handle the UI logic based on the app’s loading state.

With the introduction of UIHostingConfiguration in iOS 16, things have become much easier. Creating 2 separate custom cells is no longer required, and the implementation has become a lot more straightforward thanks to one of the view modifiers in Swift UI.

Interested to find out more? Read on!


The Sample App

Following is the sample app we are going to create in this article. It is an app that displays a list of inspiring programming quotes:

The sample app

As you can see, the app will fill up the collection view with placeholder cells while waiting for the loading to complete.

As mentioned earlier, achieving such behavior no longer required 2 separate custom cells. We can now leverage the redacted(reason:) modifier in SwiftUI to help us in generating the custom cell’s placeholder UI.

Let me show you how.


Using the “redacted(reason:)” Modifier

The redacted(reason:) modifier is a SwiftUI view modifier that let us apply a redaction to a view based on the given reason. For example, if we apply the redaction to a Text view, we will get the following output:

SwiftUI Text view with redaction effect
Text view with redaction effect

Notice that the redaction effect is generated based on the view’s content. If the Text view shows an empty string, no redaction effect will be visible.

On top of that, applying redaction to a parent view will affect the entire view hierarchy. This means that if we use the redacted(reason:) modifier on a VStack or HStack, all of the child views will get the same redaction effect.

SwiftUI VStack with redaction effect
VStack with redaction effect

Personally, I would prefer to have a view modifier that allows me to turn on/off the cell’s placeholder UI using a boolean. Thus, let’s go ahead and create that.

import SwiftUI

struct PlaceholderModifier: ViewModifier {
    
    var isPlaceholder: Bool
    
    func body(content: Content) -> some View {
        if isPlaceholder {
            // Apply a redaction to current view
            content.redacted(reason: .placeholder)
        } else {
            // Do not apply any redaction to current view
            content
        }
    }
}

extension View {
    func showAsPlacehoder(_ isPlaceholder: Bool) -> some View {
        modifier(PlaceholderModifier(isPlaceholder: isPlaceholder))
    }
}

With that, we can now enable the cell’s placeholder UI like so:

VStack {
    Image(systemName: "iphone")
    Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
}
.showAsPlacehoder(true)

Implementing Custom Cell with Placeholder UI

By using the view modifier we just created, we can implement a custom cell for our sample app like so:

import SwiftUI

struct Quote {
    let symbol: String
    let content: String
    let author: String
}

struct QuoteCell: View {

    var item: Quote
    var isLoading: Bool
    
    var body: some View {
        
        VStack(spacing: 12) {
            Image(systemName: item.symbol)
                .imageScale(.large)
                .fontWeight(.bold)
                .padding()
                .background {
                    Circle()
                        .fill(Color(.secondarySystemFill))
                }
            Text(item.content)
                .fontWeight(.bold)
            Text("- \(item.author)")
                .frame(maxWidth: .infinity, alignment: .trailing)
                .font(.footnote)
                .italic()
        }
        .frame(maxWidth: .infinity)
        .padding()
        .background {
            RoundedRectangle(cornerRadius: 12.0)
                .fill(Color(.systemFill))
        }
        .showAsPlacehoder(isLoading)   
    }
}

There is nothing fancy in the code above. It is just a basic SwiftUI view. The view isLoading property is in charge of the cell’s UI state. When isLoading is true, the cell will display its placeholder UI.


Displaying the Cell’s Placeholder UI

With the custom cell in place, we can now proceed to show the cell on a collection view. To make that happen, we need an array to control the collection view’s content, and also a boolean value to indicate the view controller’s current loading state.

private var collectionViewData = [Quote]()
private var isLoading = true

We will pass in the view controller’s isLoading property to QuoteCell during cell registration so that we can take control of when to show the cell’s placeholder UI.

quoteCellRegistration = .init { cell, indexPath, item in
    
    let hostingConfiguration = UIHostingConfiguration { [unowned self] in
        QuoteCell(item: item, isLoading: self.isLoading)
    }.margins(.horizontal, 20)
    
    // Make hosting configuration as the cell's content configuration
    cell.contentConfiguration = hostingConfiguration
}

When a user triggers a pull to refresh action, we will fill the collectionViewData with a set of hardcoded dummy data and set isLoading to true. As a result, the view controller will populate the collection view with placeholder cells. We will then start fetching data from a data provider. Once the actual data is received, we will replace the dummy data with the actual data and show them on screen.

// Note: The content of `Quote` is not important, we just need some text to generate the placeholder UI.
let dummyData = [
    Quote(symbol: "iphone", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore", author: "Author name"),
    Quote(symbol: "iphone", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore", author: "Author name"),
    Quote(symbol: "iphone", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore", author: "Author name"),
    Quote(symbol: "iphone", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore", author: "Author name"),
]

func performPullToRefresh() {
    
    refreshControl.beginRefreshing()
    
    // Load the collection view with dummy data
    isLoading = true
    collectionViewData = dummyData
    collectionView.reloadData()
    
    Task {
        // Load the collection view with data from data provider
        collectionViewData = await DataProvider.fetchData()
        collectionView.reloadData()
        isLoading = false
        
        refreshControl.endRefreshing()
    }
}

The DataProvider being used here is just a struct that acts as a fake remote server for demonstration purposes. Here’s how it looks like:

struct DataProvider {
    
    /// Sample data for demo purpose
    private static let serverData = [
        Quote(symbol: "person.2", content: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.", author: "Martin Fowler"),
        Quote(symbol: "desktopcomputer", content: "First, solve the problem. Then, write the code.", author: "John Johnson"),
        Quote(symbol: "arrow.3.trianglepath", content: "Before software can be reusable it first has to be usable.", author: "Ralph Johnson"),
        Quote(symbol: "exclamationmark.triangle", content: "The best error message is the one that never shows up.", author: "Thomas Fuchs"),
        Quote(symbol: "brain.head.profile", content: "The only way to learn a new programming language is by writing programs in it.", author: "Dennis Ritchie"),
        Quote(symbol: "house", content: "Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live", author: "John Woods"),
        Quote(symbol: "chevron.left.forwardslash.chevron.right", content: "Truth can only be found in one place: the code.", author: " Robert C. Martin"),
        Quote(symbol: "hammer", content: "In some ways, programming is like painting. You start with a blank canvas and certain basic raw materials. You use a combination of science, art, and craft to determine what to do with them.", author: "Andrew Hunt"),
        Quote(symbol: "lightbulb", content: "Testing leads to failure, and failure leads to understanding.", author: "Burt Rutan"),
        Quote(symbol: "snowflake", content: "Walking on water and developing software from a specification are easy if both are frozen.", author: "Edward V. Berard"),
    ]
    
    /// Simulate fetching data from a remote server
    static func fetchData() async -> [Quote] {
        
        // Sleep for 2s to simulate a wait when fetching data from remote server
        try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
        
        // return `serverData` shuffled
        return serverData.shuffled()
    }
}

With that, we have successfully implemented a custom cell that supports placeholder UI. You can get the full sample code here.


Further Readings


I hope you find this article helpful. If you like this article and would like to get notified when new articles come out, feel free to follow me on Twitter and subscribe to my 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.