Articles, podcasts and news about Swift development, by John Sundell.

Generating automatic placeholders for SwiftUI views

Published on 23 Jul 2020
Discover page available: SwiftUI

SwiftUI 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.