Generating automatic placeholders for SwiftUI views
Discover page available: SwiftUISwiftUI now ships with a new, built-in modifier that makes it really easy to automatically generate a placeholder for any view. For example, let’s say that we’re working on an app for reading articles, which includes the following view:
struct ArticleView: View {
var iconName: String
var title: String
var authorName: String
var description: String
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemName: iconName)
.foregroundColor(.white)
.padding()
.background(Circle().fill(Color.secondary))
VStack(alignment: .leading) {
Text(title).font(.title)
Text("By " + authorName)
.font(.subheadline)
.foregroundColor(.gray)
}
}
Text(description).padding(.top)
}
.padding()
}
}
Tip: You can use the above PREVIEW
button to see what ArticleView
looks like when rendered.
To generate a placeholder for the above view, all that we now have to do is to apply the redacted
modifier to it, specifying .placeholder
as the redaction reason:
let placeholder = ArticleView(
iconName: "doc",
title: "Placeholder",
authorName: "Placeholder author",
description: String(repeating: "Placeholder ", count: 5)
)
.redacted(reason: .placeholder)
When that modifier is applied, all texts that are displayed within a view will be replaced by grayed-out rectangles, so the actual strings that we’re passing above will only be used to determine the size of those rectangles.
Worth noting is that any images that a view contains (such as the icon displayed within our ArticleView
) will still be displayed just as they normally would. However, that’s a behavior that we can tweak using the new redactionReasons
environment value — for example by implementing a wrapper view that reads that value and then conditionally applies a modifier closure to a given view, like this:
struct RedactingView<Input: View, Output: View>: View {
var content: Input
var modifier: (Input) -> Output
@Environment(\.redactionReasons) private var reasons
var body: some View {
if reasons.isEmpty {
content
} else {
modifier(content)
}
}
}
To make the above wrapper view slightly easier to use, we could then also extend View
with a method that wraps the current view, along with a modifier to apply when the view is being redacted:
extension View {
func whenRedacted<T: View>(
apply modifier: @escaping (Self) -> T
) -> some View {
RedactingView(content: self, modifier: modifier)
}
}
With the above two pieces in place, we can now easily apply our own custom redaction logic whenever needed, for example in order to hide the Image
within our ArticleView
when generating a placeholder:
struct ArticleView: View {
...
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemName: iconName)
.foregroundColor(.white)
.whenRedacted { $0.hidden() }
.padding()
.background(Circle().fill(Color.secondary))
...
}
Text(description).padding(.top)
}
.padding()
}
}
SwiftUI also offers a way to unredact a given subview — which will make it render just as it normally would even when its parent view is being redacted. Here’s how we could use that feature to always display our article view’s title, even when a placeholder is being generated:
struct ArticleView: View {
...
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemName: iconName)
...
VStack(alignment: .leading) {
Text(title).font(.title).unredacted()
...
}
}
Text(description).padding(.top)
}
.padding()
}
}
The new redacted
modifier and its sibling APIs are all really welcome additions to SwiftUI’s built-in functionality, and should come very much in handy either when we want to render a “skeleton” version of a UI which content is being loaded, or when creating a preview instance of a widget.