Using Redacted Placeholders in SwiftUI

Learn how to apply redaction to views in SwiftUI. By Ryan Ackermann.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Redacted Placeholders

Earlier, you added two delays when loading the quotes. The first delay simulates the initial request to a network. The second you'll use to show a slower loading piece of the view's data. This is where redaction comes in.

In QuotesView, add the following immediately after the closing curly-brace of the ForEach inside body:

.redacted(
  reason: viewModel.isLoading ? .placeholder : []
)

This will make each row in the List appear redacted when isLoading is true.

Build and run.

A view full of placeholder table cells.

It's that easy to conceal parts of a view in SwiftUI: The redacted modifier will conceal the labels until loading is complete. This modifier creates great placeholder views for you.

In some situations, the automatic placeholder view isn't what works best, as you might want to make certain views always show. Fortunately for you, Apple thought of this!

Change the Image in row(from:) to the following:

Image(systemName: quote.iconName)
  .resizable()
  .aspectRatio(nil, contentMode: .fit)
  .frame(width: 20)
  .unredacted()

Build and run.

Icons showing with placeholder text.

Now the icon image always shows.

.unredacted() complements .redacted() perfectly. But in this example, the quote might take longer if it requires an additional network request to fetch its data.

Concealing User Data

While most apps use accounts to store information about users that benefits them, information is private and shouldn't be shared without consent.

For example, a stock trading app that tracks your investments should be secure. It should also be thoughtful in keeping sensitive information from prying eyes. One common trick these kinds of apps use is to hide your information when you close the app.

This will be a nice addition to your app. The quotes are not as critical as your financial records, but for a moment, pretend they are. :]

To achieve this effect, you'll reuse some of the existing logic.

Open QuotesViewModel.swift and add a new property at the top of the class:

@Published var shouldConceal = false

After that, add these three new methods to the class:

private func beginObserving() {
  // 1
  let center = NotificationCenter.default
  center.addObserver(
    self,
    selector: #selector(appMovedToBackground),
    name: UIApplication.willResignActiveNotification,
    object: nil
  )
  center.addObserver(
    self,
    selector: #selector(appMovedToForeground),
    name: UIApplication.didBecomeActiveNotification,
    object: nil
  )
}

@objc private func appMovedToForeground() {
  // 2
  shouldConceal = false
}

@objc private func appMovedToBackground() {
  // 3
  shouldConceal = true
}

Here's what this is doing:

  1. Two NotificationCenter observers will listen for notifications of the app's state.
  2. As the app moves to the foreground, it shows the quotes.
  3. When the app is closing, it conceals the quotes.

Call beginObserving at the top of init():

beginObserving()

Moving along, at the top of QuotesViewModel, add this computed property:

var shouldHideContent: Bool {
  return shouldConceal || isLoading
}

Now you'll use this new property, shouldConceal, along with the existing one, isLoading, to update the view. If either is true, the view will hide the user's content.

Back in QuotesView.swift, change the redacted modifier to use the new computed property:

.redacted(
  reason: viewModel.shouldHideContent ? .placeholder : []
)

Build and run.

Closing the app.

Now, when your app enters the background, no one can see your precious quotes! When you make the app enter the foreground again, your quotes will be restored. :]

Using redacted to create placeholder views works well for a number of situations. Not only does it look good, but it's easy to use and customize. It works well for template views and loading indicators too.

Apple's intention for creating redacted views was to use it for the revamped Home Screen widgets. That's what you'll cover next!

Creating a Widget

Apple reintroduced widgets in iOS 14. Before, they were only shown on the Today View. The old implementation of widgets didn't have a good loading state; they started off blank and often took a while to load.

Now that widgets are front and center on the Home Screen, there's more of a need for a better loading state. The solution is a straightforward modifier that applies to any View. Since any third-party app can offer its own widget, this generic solution is perfect.

The widget in this app shows a new quote each hour. The design will work like the main app. In the case where the system is showing a placeholder, you still want the icon to show up.

Navigate to Widget/QuoteOfTheHour.swift and change the implementation of getTimeline(in:completion:) to the following:

var entries: [QuoteEntry] = []
// 1
var quotes = ModelLoader.bundledQuotes

let calendar = Calendar.current
let currentDate = Date()

for hourOffset in 0..<24 {
  // 2
  guard let entryDate = calendar.date(
    byAdding: .hour, 
    value: hourOffset, 
    to: currentDate) 
  else {
    continue
  }

  // 3
  guard let randomQuote = quotes.randomElement() else {
    continue
  }

  // 4
  if let index = quotes.firstIndex(of: randomQuote) {
    quotes.remove(at: index)
  }

  entries.append(QuoteEntry(model: randomQuote, date: entryDate))
}

// 5
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)

In the code above:

  1. Quotes are pulled from the shared model loader.
  2. A new quote is scheduled for each hour for the next 24 hours.
  3. A quote is chosen at random.
  4. The selected quote is removed so that scheduled quotes are unique.
  5. The timeline is set using the 24 selected quotes.

Now that there's data to work with, the next step is to design the widget.

Replace the body of QuoteOfTheHourEntryView in QuoteOfTheHour.swift with the following:

VStack(alignment: .leading) {
  HStack {
    Image(systemName: entry.model.iconName)
      .resizable()
      .aspectRatio(nil, contentMode: .fit)
      .frame(width: 12)

    Spacer()

    Text(entry.model.createdDate, style: .date)
      .font(
        .system(
          size: 12,
          weight: .bold,
          design: .rounded
        )
      )
      .foregroundColor(.secondary)
      .multilineTextAlignment(.trailing)
  }

  Text(entry.model.content)
    .font(
      .system(
        size: 16,
        weight: .medium,
        design: .rounded
      )
    )

  Spacer()
}
.padding(12)

This is similar to the rows used in the main app, with the difference that the font sizes and the icon are smaller.

Build and run. Close the app and add a widget to the Home Screen like so:

Adding a Home Screen widget.

The final thing is to make sure the icon in the widget is always displayed. Since this app uses SF Symbols for the icons, they're always accessible. The OS loads the quote and date for the widget from the timeline, so they'll be redacted during that time.

Like the main app, a single modifier will ensure the icon will show up.

Replace the Image in the body with the following:

Image(systemName: entry.model.iconName)
  .resizable()
  .aspectRatio(nil, contentMode: .fit)
  .frame(width: 12)
  .unredacted()

Build and run.

A partially redacted widget.

When the system is loading the widget, it'll show a placeholder with the icon present.

Note: The widget should load right away in the simulator. A redacted modifier was added to the widget for this screenshot.

Now the widget looks great in all scenarios.