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

Have you ever used a mobile app or website that took a while to load? Slow connection speeds aren’t pleasant to deal with, are they? It’s even worse when you can’t tell if content is loading or if it failed along the way.

Fortunately, there are a few ways to inform a user when something is taking longer than expected. One of the most modern approaches is using redacted placeholders. These were introduced to SwiftUI in iOS 14.

In this tutorial, you’ll learn:

  • How to leverage placeholders in SwiftUI
  • Why loading states are so important
  • Best practices for concealing private user information
  • How to create a widget

Placeholders are a more modern approach that showcase a preview of a UI. This design pattern is commonly used in text fields, where a field displays a prompt that helps the user know what to enter.

Another strength of placeholders is the ability to conceal private information. Financial apps will typically do this when the app enters the background. In SwiftUI, it’s easier to show a placeholder rather than create a separate view to conceal the sensitive information.

So without any further ado, it’s time to learn how to do just that!

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Inside the ZIP, you’ll find two folders, final and starter. Open the starter folder. The project consists of an app that displays a header with the app’s name, Quotation.

Included with the project is a JSON file containing motivational quotes. This file is located at Supporting Files/quotes.json. Each quote has an ID, date and icon name — along with the quote itself. You’ll find the data model for this data at Shared/Quote.swift. The quotes in this data set are from motivationping.com.

The aim of this tutorial is to showcase how important loading states are in software. It’ll show how to do this in an app and an iOS 14 widget.

Requesting Quotes

The first thing you’ll need is to get the quotes loaded into the app. Open the view model located at App/QuotesViewModel.swift. This is where you’ll load the quotes. Add the following properties to the top of QuotesViewModel:

@Published var isLoading = false
@Published var quotes: [Quote] = []

The first property determines if content is loading. The second is the array of quotes the app will display. Since the app will have a widget, it’s a good idea to share the loading logic.

Open Shared/ModelLoader.swift and change the contents of bundledQuotes to the following:

// 1
guard let url = Bundle.main
  .url(forResource: "quotes", withExtension: "json") 
else {
  return []
}

// 2
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970

do {
  // 3
  let data = try Data(contentsOf: url)
  return try decoder.decode([Quote].self, from: data)
} catch {
  print(error)
  return []
}

Here’s what the code above is doing:

  1. This is the path to the JSON file.
  2. This creates a JSONdecoder used to parse the quotes.
  3. The decoder attempts to read the data from the file and returns the decoded quote array to bundleQuotes.

Now you have a way to read the bundled data. Head back to QuotesViewModel.swift and add the following to the end of the class:

init() {
  withAnimation {
    self.quotes = ModelLoader.bundledQuotes
  }
}

This initializer takes care of loading the quotes from the disk by calling your new method.

Nothing will show up yet since there isn’t a UI to display the quotes. Don’t worry! You’ll get to that next.

Open App/QuotesView.swift and add the following under body:

private func row(from quote: Quote) -> some View {
  // 1
  HStack(spacing: 12) {
    // 2
    Image(systemName: quote.iconName)
      .resizable()
      .aspectRatio(nil, contentMode: .fit)
      .frame(width: 20)

    // 3
    VStack(alignment: .leading) {
      Text(quote.content)
        .font(
          .system(
            size: 17,
            weight: .medium,
            design: .rounded
          )
        )

      Text(quote.createdDate, style: .date)
        .font(
          .system(
            size: 15,
            weight: .bold,
            design: .rounded
          )
        )
        .foregroundColor(.secondary)
    }
  }
}

Going through the code:

  1. This row view shows an icon on the left and text on the right.
  2. The quote data from the bundled JSON file contains SF Symbols. This Image will display the desired symbol.
  3. The quote and its date are displayed one on top of the other.

Now, add the following to the List block in the body:

ForEach(viewModel.quotes) { quote in
  row(from: quote)
}

This loops through the quotes and loads them into the view. Build and run.

View of quotes.

Great, you can now see the bundled quotes!

Showing Progress

In ideal situations, information loads right away and no errors occur. However, with cellular networks and complex server-side code, something can and will go wrong, so it’s important to try and make these kinds of situations as smooth as possible for users.

Even local information can take some time to load. A database query resulting in more than 100,000 items would take a few seconds at least. Currently, the quotes load without delay or issue. But, for the sake of this tutorial, you’ll add an artificial delay to simulate a slow network connection.

Open QuotesViewModel.swift and add the following below init():

private func delay(interval: TimeInterval, block: @escaping () -> Void) {
  DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
    block()
  }
}

This helper method runs a closure after a specified period of time using Grand Central Dispatch. You’ll use this to delay a few parts of the loading process.

Next, replace the contents of init() with this:

isLoading = true
let simulatedRequestDelay = Double.random(in: 1..<3)

delay(interval: simulatedRequestDelay) {
  withAnimation {
    self.quotes = ModelLoader.bundledQuotes
  }

  let simulatedIngestionDelay = Double.random(in: 1..<3)

  self.delay(interval: simulatedIngestionDelay) {
    self.isLoading = false
  }
}

This code adds two delays to the mix. You update the isLoading property here to add an extra layer of progress. Two random numbers help simulate a real work scenario.

Build and run.

An empty loading screen.

You'll now see an empty view until the quotes load. In a production app, behavior like this is confusing to a user. It isn't clear if something is happening or if anything failed.

One of the most common UI patterns used to communicate that data is loading is a spinner. Before the introduction of iPhone X, the easiest way to show a loading spinner was UIApplication.isNetworkActivityIndicatorVisible. Other popular patterns include loading bars, blurs and placeholders.

The first improvement you'll make is to add a loading indicator. This is known as a UIActivityIndicatorView in UIKit or a ProgressView in SwiftUI. The view model is already set up to do this.

Open QuotesView.swift and, inside QuotesView, replace the contents of body with the following:

ZStack {
  NavigationView {
    List {
      ForEach(viewModel.quotes) { quote in
        row(from: quote)
      }
    }
    .navigationTitle("Quotation")
  }

  if viewModel.quotes.isEmpty {
    ProgressView()
  }
}

The main difference here is the use of a ZStack to position the progress view above the List. If an error occurs, the progress view stops, signaling that something unexpected happened.

More often that not, it takes a number of network requests to retrieve all the required data for a view. Take the example of a view that loads the contents of a shopping cart. This view would need to make requests for each product image or user review. By the time the page has fully loaded, there could have been dozens of requests made.

Build and run.

A view loading with a centered spinner.

This ProgressView goes a long way to show the user something is happening!