EP 117 · The Point of Redacted SwiftUI · Sep 14, 2020 ·Members

Video #117: The Point of Redacted SwiftUI: Part 1

smart_display

Loading stream…

Video #117: The Point of Redacted SwiftUI: Part 1

Episode: Video #117 Date: Sep 14, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep117-redacted-swiftui-the-point-part-1

Episode thumbnail

Description

Not only can we easily redact the logic from a view using the Composable Architecture, but we can also insert new logic into our views without making any changes to the core logic of the view. To demonstrate this we will build an onboarding flow for a more complex application.

Video

Cloudflare Stream video ID: 4650200c133b2fd8b5b014b7f429a73b Local file: video_117_redacted-swiftui-the-point-part-1.mp4 *(download with --video 117)*

References

Transcript

0:05

This is so incredibly powerful, and this is what it means to take “separation of concerns” seriously. We have truly separated the logic from the view, so much so that we can wholesale replace the logic for a view with anything we want. What’s the point?

0:37

So this is definitely cool, but on Point-Free we like to end every episode by asking “what’s the point?” This is our chance to get real with our viewers and try to really convince everyone that these techniques are useful in everyday code and not just something that looks fancy at first glance.

1:01

And in this case it’s important to ask because as we saw earlier in the episode we were able essentially replicate this functionality in a vanilla SwiftUI application by just disabling the whole view. So, what’s the point of demonstrating how redactions and placeholders work with the Composable Architecture when regular SwiftUI applications seem to work well enough with these new APIs?

1:23

Well, while it is true that a vanilla SwiftUI was able to mostly recreate what the Composable Architecture accomplished, we don’t believe that is always the case. We think there are some really interesting use cases for redaction that go well beyond just showing some placeholders while loading data. We think that redactions can be integral tool for building many types of rich user experiences, and the Composable Architecture only enhances our ability to build these experiences.

1:52

To prove this, we are going to add a new feature to a demo application that is in the Composable Architecture repo. We are going to add some pretty amazing functionality to it, and it will almost be entirely additive code outside the core feature. So let’s dig in… Redacting a todo app

2:12

If you haven’t seen it already, the Composable Architecture repo has lots of case studies and demo applications to show how to solve every day problems using the library. One particularly interesting application is this todo app. It has the basic features of adding todos, editing todos, filtering, clearing completed ones, and more.

2:42

Now before we add any new functionality to this application, let’s explore a bit how the redaction API and the Composable Architecture play together.

2:50

To start, let’s just redact the entire application in our SwiftUI preview: AppView( store: Store( initialState: AppState(todos: .mock), reducer: appReducer, environment: AppEnvironment( mainQueue: DispatchQueue.main.eraseToAnyScheduler(), uuid: UUID.init ) ) ) .redacted(reason: .placeholder)

3:03

Interestingly this redacts everything, even the navigation buttons at the top and the labels on the segmented picker control.

3:11

But, as we’ve seen before, just because the views are redacted does not mean the logic is redacted. Right now we can tap on buttons, change filters, and even complete todos, all without even being able to see the underlying data.

3:32

Well, luckily since this app was built with the Composable Architecture we have a super simple way to redact the logic too. We can swap out the live appReducer for the .empty reducer, which simply lets every action go by without making any mutations to state or executing any effects: reducer: .empty,

3:44

Now when we run the preview we will see that tapping on any button does absolutely nothing.

4:02

This is really cool, we only had to change a single line in order to make sure this view doesn’t accidentally leak any functionality into the real world by using placeholder data. We can even take this a step further by redacting the environment so that the view literally doesn’t even have access to dependencies, and so it truly cannot make any changes to the outside world: environment: () // AppEnvironment( // mainQueue: DispatchQueue.main.eraseToAnyScheduler(), // uuid: UUID.init // )

4:40

Now although this is cool, we can push it further. SwiftUI comes with an API that allows us to unredact a view which causes the view to be rendered normally, even if the parent view was redacted.

4:52

For example, suppose we want the filter picker control to be visible while todos are loading. We can simply unredact it: .pickerStyle(SegmentedPickerStyle()) .unredacted()

5:09

However, now the filter is completely inoperable because we are using the .empty reducer. You can tap on any of the filters, but the filter won’t actually change because no logic is being executed.

5:25

Although so far we have only wholesale redacted all of a screen’s logic, there’s nothing stopping us from implementing a little bit of custom logic that works just for redacted views. Say we want the logic that allows the filter to change, but nothing else. Then we can just create a little custom reducer right when creating the store and view: reducer: Reducer { state, action, _ in switch action { case let .filterPicked(filter): state.filter = filter return .none default: return .none } },

6:25

And now when we run the preview we will see that the filtering control works, yet everything else is still disabled. This is pretty amazing. We were able to selectively add back a bit of logic to our view while the placeholders are being shown. There really is no concept of doing this kind of high level manipulation of the application’s logic when building a SwiftUI application in the more vanilla style.

6:57

And if you are a little worried about replicating logic in this little placeholder reducer, then there are two things you can do that may make you feel better. First, you can write tests. You can write a snapshot test of how your view behaves with this placeholder store plugged in, and that will give you test coverage that you are replicating the exact subset of logic you expect.

7:19

Second, you can alternatively call out to the real life reducer on the inside of this placeholder reducer. We can just pass along the state, action and environment to the appReducer : case .filterPicked: // state.filter = filter // return .none return appReducer.run(&state, action, environment)

7:35

However, in order for this to work we now need to start passing a live environment rather than the void one with no dependencies: environment: // () AppEnvironment( mainQueue: DispatchQueue.main.eraseToAnyScheduler(), uuid: UUID.init )

7:46

This is a little scary to do. It may not be so bad right this moment because our environment only holds a main queue and UUID initializer, but in the future we may have some serious dependencies in here that talk to the outside world, such as API clients, analytics clients, and more. You might not want to accidentally track analytics events when placeholder is being displayed. Preventing that was one of the big benefits to putting in the void environment a moment ago.

8:14

Well, there’s a great solution to this as well. We can simply make placeholder dependencies that act as a no-op for all of its endpoints. For example, what if we had an analytics client with a track endpoint used to track an event with some properties: struct AnalyticsClient { var track: (String, [String: String]) -> Effect<Never, Never> }

9:01

In production we would have a “live” client that actually hits the network. extension AnalyticsClient { static let live = Self( track: { name, properties in .fireAndForget { // track event } } ) }

9:37

But this live client is exactly what we don’t want our placeholder to have access to. We don’t want to accidentally muddy our analytics with placeholder interactions. So we can instead introduce a “placeholder” client that simply ignores the data and returns an effect that does nothing. extension AnalyticsClient { static let placeholder = Self( track: { _, _ in .none } ) }

10:01

And if we introduce this client to the environment, then where we construct our placeholder view we could pass a placeholder analytics client along. struct AppEnvironment { var analytics: AnalyticsClient … } environment: AppEnvironment( analytics: .placeholder, … )

10:22

So this means we can still call out to live logic in our application if we wish, but in order to be safe that we never actually change anything in the real world while interacting with placeholder data we will provide placeholder dependencies.

10:34

This is so incredibly powerful. We are opting in and out of subsets of our application as if it were an a la carte menu. We can choose to plug in no logic, some custom placeholder logic that we implement right in line, or we can even call out to the live application logic yet still guarantee that that live logic will never make changes to the outside world. Pretty incredible!

10:55

But it gets even better. Todo onboarding

10:59

We can make this a la carte selection of placeholder logic dynamic, so it encapsulates its own logic, allowing the placeholders to be interactive in a way that is separate from the core application logic.

11:12

To demonstrate this we are going to build an onboarding experience for this todo application that is supposed to help teach the user how to use the app. It could be a multi-step process that highlights various parts of the screen and gives a little info on how to use that particular feature. For example:

11:29

We could highlight the the top navigation and say this where you can do mass operations on the list of todos, such as clearing all the completed ones or deleting todos, as well as adding a new todo.

11:40

We could highlight the filter control and say this is where you can get a more concise list of your todos based on them being completed or not.

11:49

And then finally we could highlight the todos list and say that you can complete todos by tapping the checkbox and can edit a todos title by tapping on the current title.

11:58

This is quite a complex addition to make to our feature, and if that wasn’t enough we also want to do it by making the fewest number of changes possible to the core todo feature. We’d like the onboarding code to be mostly additional code that exists outside the todo code. Ideally the core todo feature could even live in its own module with no dependency whatsoever on the onboarding feature.

12:29

We want this because the todo feature is complicated on its own, and the onboarding code is also going to be pretty complicated, and so if we had to sprinkle a little bit of onboarding code in every dark corner of the todo code we are in for a world of hurt. We will eventually get to a point where we couldn’t possibly keep all the ins and outs of the feature in our heads at once, and the code will be hard to maintain.

12:55

In the end we will be able to implement this onboarding feature and only have to change 4 lines of code in our todo feature. That’s it, just four!

13:13

Let’s start by getting a bit of domain in place. The onboard experience we just described consists of 3 steps, so let’s create an enum to describe those steps: enum OnboardingStep: Equatable { case actions case filters case todos }

13:43

Let’s also add some helper computed variables for moving to the next and previous step: var next: Self? { switch self { case .actions: return .filters case .filters: return .todos case .todos: return nil } } var previous: Self? { switch self { case .actions: return self case .filters: return .actions case .todos: return .filters } }

14:02

Next we are going to implement a view that houses the onboarding UI. This view will overlay some UI on top of the actual todo application so that it can present contextual information. We will call this view OnboardingView : struct OnboardingView: View { @State var step: OnboardingStep? var body: some View { EmptyView() } }

14:42

Right now we are using @State to represent this state, but soon it will be powered by a Composable Architecture Store . It is optional because a nil value will represent that we are not in the onboarding phase of the application, and so should show the full version of the app.

14:50

We can start the view by trying to unwrap the step and let that determine what we show: if let step = self.step { // show onboarding } else { // show live todo app }

15:03

In the onboarding branch of the if we want to show a view overlaid on top of a version of the todo application. We can do this with a ZStack : ZStack { }

15:20

And then inside the ZStack we’ll put a VStack to hold the onboarding views, and we’ll use a spacer to push that to the bottom ZStack { VStack { Spacer() } }

15:42

In this VStack we are going to put a button for going to the previous onboarding step, a text view that shows some information about the current step we are on, then a button to go to the next onboarding step, and then finally a skip button for bailing out of the onboarding flow early: HStack(alignment: .top) { Button(action: { }) { Image(systemName: "chevron.left") } .frame(width: 44, height: 44) .foregroundColor(.white) .background(Color.gray) .clipShape(Circle()) .padding([.leading, .trailing]) Spacer() VStack { switch step { case .actions: Text( """ Use the navbar actions to mass delete todos, \ clear all your completed todos, or add a new one. """ ) case .filters: Text( """ Use the filters bar to change what todos are \ currently displayed to you. Try changing a filter. """ ) case .todos: Text( """ Here's your list of todos. You can check one \ off to complete it, or edit its title by tapping \ on the current title. """ ) } Button("Skip") { } .padding() } Spacer() Button(action: { }) { Image(systemName: "chevron.right") } .frame(width: 44, height: 44) .background(Color.gray) .foregroundColor(.white) .clipShape(Circle()) .padding([.leading, .trailing]) }

16:19

The action closures for these buttons can be implemented by just mutating the state to take us to the next or previous step: Button(action: { self.step = self.step?.previous }) { … } … Button(action: { self.step = nil }) { … } … Button(action: { self.step = self.step?.next }) { … }

16:50

And we’ll add a gradient to the background of this HStack so that it sit on top of the todo app and provide enough contrast to read. We’ll also add some extra padding to make sure the gradient extends far enough to cover a large area around the view: .padding(.top, 400) .padding(.bottom, 100) .background( LinearGradient( gradient: .init( colors: [ .init(white: 1, opacity: 0), .init(white: 0.8, opacity: 1) ] ), startPoint: .top, endPoint: .bottom ) )

17:15

This is the basics of our onboarding view, and we can get a preview in place to see how it looks: struct OnboardingView_Previews: PreviewProvider { static var previews: some View { OnboardingView(step: .actions) } }

17:47

Not bad! We can already tap on the arrow buttons and see the onboarding info change.

18:04

Now the question is: how can we hook up this functionality to the actual todo app? We want to render the todo view underneath this overlay such that it shows placeholder data and logic, and we want to redact only a subset of the UI depending on what step of the onboarding process we are on.

18:23

The place to put this is in the ZStack right under the VStack that holds the onboarding UI, and we start start by putting in some placeholder state and the empty reducer: ZStack { AppView( store: Store( initialState: AppState(todos: .mock), reducer: .empty, environment: () ) ) .redacted(reason: .placeholder)

19:28

Now we’re starting to see the beginnings of what this onboarding experience can look like. We’ve got some UI at the bottom to describe what is on screen, and then underneath that is our real life application running with placeholder data and logic.

19:50

We can also put the live application with its live state and logic in the else branch: } else { AppView(store: self.store) }

20:06

But in order to do that we need to provide a store to the view: struct OnboardingView: View { @State var step: OnboardingStep? let store: Store<AppState, AppAction> … }

20:14

And construct that store in the preview: static var previews: some View { OnboardingView( step: .actions, store: Store( initialState: .init(), reducer: appReducer, environment: .init( analytics: .live, mainQueue: DispatchQueue.main.eraseToAnyScheduler(), uuid: UUID.init ) ) ) }

21:04

And the preview can now walk through all of the steps in a placeholder fashion before launching into the real app experience.

21:14

OK, we’ve now got a lot of foundation in place for the onboarding experience, but it’s now time to start hooking up some real logic. We want the buttons in the onboarding UI to move us through the steps of onboarding, and we somehow want the steps to cause the todo application to redact only certain parts of the UI. For example, when we are on the filters step we should only redact the top navigation and the todos list.

21:55

We can accomplish this by using a wonderful feature of SwiftUI known as “environment values”. This allows you to push values through to deep parts of the view hierarchy with very little work. We just need to tack on an .environment view modifier and describe what data we want to send through and under what key: AppView( store: Store( initialState: AppState(todos: .mock), reducer: .empty, environment: () ) ) .environment(\.onboardingStep, self.step) .redacted(reason: .placeholder)

22:23

But in order to set this environment value for a particular key we need to create some environment keys: struct OnboardingStepEnvironmentKey: EnvironmentKey { static var defaultValue: OnboardingStep? = nil } extension EnvironmentValues { var onboardingStep: OnboardingStep? { get { self[OnboardingStepEnvironmentKey.self] } set { self[OnboardingStepEnvironmentKey.self] = newValue } } }

23:44

Now this compiles, and it means we can capture this environment variable in our todo screen so that we know what step of the onboarding flow we are on. We will use this information to un-redact certain parts of the UI.

24:11

So we start by adding a new field to our AppView that pulls its data from the environment: struct AppView: View { let store: Store<AppState, AppAction> @Environment(\.onboardingStep) var onboardingStep … }

24:32

And then we want to use this value to determine which views we want to unredact. Unfortunately the .unredacted API doesn’t support putting logic in its determination like the .redacted API does. We’d like to be able to do something like: .unredacted(if: self.onboardingStep == .filters)

25:05

Just as we were able to do this in the articles app: .redacted(reason: self.viewModel.isLoading ? .placeholder : [])

25:27

To recreate this we will create a helper that allows us to apply the unredacted modifier only if a certain condition is met: extension View { @ViewBuilder func unredacted(if condition: Bool) -> some View { if condition { self.unredacted() } else { self } } }

26:17

And now we can redact the todos list in the case that we are on the todos step of the onboarding experience: .unredacted(if: self.onboardingStep == .todos)

26:28

And finally we can redact the navigation button when we are on the actions step: .unredacted(if: self.onboardingStep == .actions)

26:42

Hopefully SwiftUI will provide this kind of API for us someday.

26:50

Amazingly this will be the only changes we have to make to this view. Just these 4 lines: adding an environment value, and 3 un-redactions. We’d love if there was a way to even remove these lines, and have the onboarding feature be 100% separated from the todo feature, but we aren’t sure how to do that right now, and I think just 4 additional lines is pretty good.

28:33

With this done we have a mostly functional onboarding experience. When we step through the onboarding flow we will see that the redaction focus changes from the nav to the filters and then finally to the todo list. And each step of the way the UI does not allow any real logic to be executed. You can tap on any button and nothing happens. Next time: guided onboarding

28:57

But we can take this even further. What if we wanted certain parts of the application’s logic to be active while in a particular step of the onboarding process? For example, while on the actions step we could allow you to add todos, and on the filters step maybe we allow you to switch the filters, and while on the todos step we allow you to check off todos. This would enhance the user experience by allowing the user to explore certain functionality in the app while still operating within the onboarding sandbox.

29:29

To do this we are going to implement the onboarding domain as a whole new Composable Architecture feature. That means we will specify its state, actions and reducer, and use a store to drive our OnboardingView . References redacted(reason:) Apple’s new API for redacting content in SwiftUI. https://developer.apple.com/documentation/swiftui/view/redacted(reason:) Separation of Concerns “Separation of Concerns” is a design pattern that is expressed often but is a very broad guideline, and not something that can be rigorously applied. https://en.wikipedia.org/wiki/Separation_of_concerns Downloads Sample code 0117-redacted-swiftui-pt3 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .