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:
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:
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.
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
- Handling Cell Interactions When Using UIHostingConfiguration in iOS 16
- How to Refresh Cell’s Content When Using UIHostingConfiguration
- How to Create Custom Header & Footer Using UIHostingConfiguration
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.