How to create custom redacted effects

With the recent release of Xcode 12 we've gained a new .redacted(reason:) SwiftUI modifier.

See last week's article for a quick refresher.

This new modifier accepts an instance of RedactionReasons, which, as of beta 3, has one option available: .placeholder.

While this is great, and I'm sure more options will come in the future, the effects are not very customizable:
in this article, let's see how expand what SwiftUI offers with our own effects.

Extending RedactionReasons

If we target iOS 14 and later, one way to do so is to piggyback on the current APIs and define our own RedactionReasons instances, as this type conforms to OptionSet:

extension RedactionReasons {
  public static let confidential = RedactionReasons(rawValue: 1 << 10)
}

The high number was chosen to avoid clashes with future additions from the SwiftUI team.

We can then create a new view modifier that checks for the redactionReasons environment, and, if our reason is found, modify the view:

struct Redactable: ViewModifier {
  @Environment(\.redactionReasons) private var reasons

  @ViewBuilder
  func body(content: Content) -> some View {
    if reasons.contains(.confidential) {
      content
        .accessibility(label: Text("Confidential"))
        .overlay(Color.black)
    } else {
      content
    }
  }
}

At call site, instead of calling .modifier(Redactable()) wherever needed, we can create a View extension:

extension View {
  func redactable() -> some View {
    modifier(Redactable())
  }
}

Here's how we would use it:

struct ContentView: View {
  var body: some View {
    VStack {
      Text("Hello world")

      Text("Hello world")
        .redactable()
    }
    .redacted(reason: .confidential)
    .font(.title)
  }
}

The final gist can be found here.

While this works, we're using APIs that we don't own and don't have any inner insights: this makes our solution fragile. The SwiftUI team can, unintentionally, break our code in any future release.

To avoid this, we can create our own API.

Building our own Redacted API

Let's start by defining our reasons:

public enum RedactionReason {
  case placeholder
  case confidential
  case blurred
}

This enum definition doesn't clash with SwiftUI's RedactionReasons because we omit the s at the end of the type (SwiftUI RedactionReasons is an OptionSet, hence the plural in the type name).

Then we define a modifier for each of our reasons:

struct Placeholder: ViewModifier {
  func body(content: Content) -> some View {
    content
      .accessibility(label: Text("Placeholder"))
      .opacity(0)
      .overlay(
        RoundedRectangle(cornerRadius: 2)
          .fill(Color.black.opacity(0.1))
          .padding(.vertical, 4.5)
    )
  }
}

struct Confidential: ViewModifier {
  func body(content: Content) -> some View {
    content
      .accessibility(label: Text("Confidential"))
      .overlay(Color.black)
    )
  }
}

struct Blurred: ViewModifier {
  func body(content: Content) -> some View {
    content
      .accessibility(label: Text("Blurred"))
      .blur(radius: 4)
  }
}

As we did before, we then define a Redactable view modifier:

struct Redactable: ViewModifier {
  let reason: RedactionReason?

  @ViewBuilder
  func body(content: Content) -> some View {
    switch reason {
    case .placeholder:
      content
        .modifier(Placeholder())
    case .confidential:
      content
        .modifier(Confidential())
    case .blurred:
      content
        .modifier(Blurred())
    case nil:
      content
    }
  }
}

As described in the conclusions, there's little gain in making our RedactionReason available in the environment instead of passing it directly to the view modifier. Therefore I opted for the simpler API.

Lastly, let's create the View extension to be used at call site:

extension View {
  func redacted(reason: RedactionReason?) -> some View {
    modifier(Redactable(reason: reason))
  }
}

Here's an example on how to use it:

struct ContentView: View {
  var body: some View {
    VStack {
      Text("Hello World")
        .redacted(reason: nil)
      Text("Hello World")
        .redacted(reason: .placeholder)
      Text("Hello World")
        .redacted(reason: .confidential)
      Text("Hello World")
        .redacted(reason: .blurred)
    }
  }
}

And just like that, we have our own redacted API, compatible with iOS 13 as well.

The final gist can be found here.

Conclusions

SwiftUI's new modifier is great and I'm sure we will find plenty of awesome uses for it, especially when more effects will be added. For the moment, if what SwiftUI offers is not enough for our needs, we can create our own effects without too much trouble.

To be fair, as we don't own the actual rendering of each view, our custom effects are more limited than SwiftUI's .redacted ones:
for example there's no easy way for us to get the environment's .foregroundColor (FB8161189), and our effects take place even when a nested view has been marked with .unredacted().

What effects would you like to see implemented? Do you already a feature in mind where you will use .redacted? Please let me know!

As always, thank you for reading and stay tuned for more SwiftUI articles!

⭑⭑⭑⭑⭑

Further Reading

Explore SwiftUI

Browse all