The magic of redacted modifier in SwiftUI
Redacted modifier is the thing that will have a great impact on how iOS apps handle loading states. During WWDC20, Apple showed us the easy way of hiding the data from home-screen widgets using the redacted modifier. Today we will talk about using the redacted modifier to hide sensitive data and handle loading states.
The all-in-one macOS app that enhances App Store Connect, supercharging your app updates, localization, and ASO with AI-powered tools. Save 25% and try now!
Redacted modifier
The redacted modifier transforms the view hierarchy into a skeleton view when added. Don’t worry if you are not familiar with the skeleton view pattern. You will see how it works very soon. Assume that you are working on the Github app. You have a view that represents a repo on the list.
struct Repo: Hashable, Decodable {
let name: String
let description: String
let stars: Int
}
struct RepoView: View {
let repo: Repo
var body: some View {
HStack {
VStack(alignment: .center) {
Image(systemName: "star.fill")
.resizable()
.frame(width: 44, height: 44)
Text(String(repo.stars))
.font(.title)
}.foregroundColor(.red)
VStack(alignment: .leading) {
Text(repo.name)
.font(.headline)
Text(repo.description)
.foregroundColor(.secondary)
}
}
}
}
Let’s create a sample data that we can use to preview our RepoView.
extension Repo {
static let mock = Repo(
name: "SwiftUICharts",
description: "A simple line and bar charting library that support accessibility written using SwiftUI. ",
stars: 579
)
}
Now we can use our RepoView in preview to see how it looks with or without a redacted modifier.
struct ContentView: View {
var body: some View {
HStack {
RepoView(repo: .mock)
Divider()
RepoView1(repo: .mock)
.redacted(reason: .placeholder)
}
}
}
As you can see in the example above, we have a plain RepoView on the left and a redacted version on the right. The redacted modifier transforms images and text views in the view hierarchy to hide its content using overlays. Let’s take a look at a more advanced example.
final class Store: ObservableObject {
@Published private(set) var repos: [Repo]
@Published private(set) var isLoading = false
private let service: GithubService
init(
service: GithubService,
initialState: [Repo] = Array(repeating: .mock, count: 5)
) {
self.repos = initialState
self.service = service
}
func fetch() {
isLoading = true
service
.fetchRepos(matching: "SwiftUI")
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.handleEvents(receiveCompletion: { [weak self] _ in self?.isLoading = false})
.assign(to: &$repos)
}
}
Here we have a store object that handles the data loading. We will use the redacted modifier to hide the mock data that we have as our store object’s initial state.
To learn more about store objects, take a look at my “Modeling app state using Store objects in SwiftUI” post.
struct ContentView: View {
@StateObject var store = Store(service: .init())
var body: some View {
List(store.repos, id: \.self) { repo in
RepoView(repo: repo)
}
.onAppear(perform: store.fetch)
.redacted(reason: store.isLoading ? .placeholder : [])
}
}
While attaching the redacted modifier, we have to provide an instance of RedactionReasons struct using the reason parameter. RedactionReasons is an option set that we can extend with as many reasons as we need. RedactionReasons struct provides us a ready to use placeholder instance that we use in the example above.
Remember that the redacted modifier hides the data only visually. It is still clickable in case of buttons. It is your responsibility to disable buttons while using the redacted modifier.
Unredacted modifier
As we already know, the redacted modifier traverses the view hierarchy and applies its effect to hide the actual data, but what if we want to keep a certain part of the view visible? SwiftUI provides us another modifier called unredacted. Unredacted modifier allows us to keep the view unredacted while applying the redacted modifier.
struct RepoView: View {
let repo: Repo
var body: some View {
HStack {
VStack(alignment: .center) {
Image(systemName: "star.fill")
.resizable()
.frame(width: 44, height: 44)
.unredacted()
Text(String(repo.stars))
.font(.title)
}.foregroundColor(.red)
VStack(alignment: .leading) {
Text(repo.name)
.font(.headline)
Text(repo.description)
.foregroundColor(.secondary)
}
}
}
}
Reasons
As we learned, the redacted modifier accepts a reason parameter. It’s great that we can create as many different reasons and hide only the part we need. SwiftUI provides a special environment value called redactionReasons to get the redaction reason applied to the current view hierarchy. Let’s start first with the extending RedactionReasons struct with more options.
extension RedactionReasons {
static let text = RedactionReasons(rawValue: 1 << 2)
static let images = RedactionReasons(rawValue: 1 << 4)
}
To learn more about OptionSet protocol in Swift, take a look at my “Inclusive enums with OptionSet” post.
Now we can tune our RepoView to redact the only needed parts of the view.
struct RepoView1: View {
@Environment(\.redactionReasons) var reasons
let repo: Repo
var body: some View {
HStack {
VStack(alignment: .center) {
Image(systemName: "star.fill")
.resizable()
.frame(width: 44, height: 44)
.unredacted(when: !reasons.contains(.images))
Text(String(repo.stars))
.font(.title)
.unredacted(when: !reasons.contains(.text))
}.foregroundColor(.red)
VStack(alignment: .leading) {
Text(repo.name)
.font(.headline)
Text(repo.description)
.foregroundColor(.secondary)
}.unredacted(when: !reasons.contains(.text))
}
}
}
Remember that SwiftUI applies skeleton view effect only when we use placeholder redaction reason. Any other reasons should be handled manually.
extension View {
@ViewBuilder func unredacted(when condition: Bool) -> some View {
if condition {
unredacted()
} else {
// Use default .placeholder or implement your custom effect
redacted(reason: .placeholder)
}
}
}
Conclusion
Today we learned another great feature that SwiftUI provides us for free out of the box. I really love the skeleton view pattern, and with SwiftUI, I started using it on every screen that loads some data. I hope you enjoy the post. Feel free to follow me on Twitter and ask your questions related to this article. Thanks for reading, and see you next week!