Video #118: The Point of Redacted SwiftUI: Part 2
Episode: Video #118 Date: Sep 21, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep118-redacted-swiftui-the-point-part-2

Description
We finish building a rich onboarding experience for our application by selectively enabling and disabling pieces of logic in the app depending on what step of the onboarding process we are on. This is only possible due to the strict “separation of concerns” the Composable Architecture maintains.
Video
Cloudflare Stream video ID: cff40ba224fac2e0371ec4e0b44a2684 Local file: video_118_redacted-swiftui-the-point-part-2.mp4 *(download with --video 118)*
References
- Discussions
- redacted(reason:))
- Separation of Concerns
- 0118-redacted-swiftui-pt4
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
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.
— 0:29
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.
— 1:01
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 . Making onboarding interactive
— 1:14
The core state for the onboarding experience is the current step we are on, as well as a version of the AppState that acts as the placeholder data to show in the todo screen that is below the onboarding UI. struct OnboardingState: Equatable { var placeholderState: AppState var step: OnboardingStep? }
— 1:36
And the core actions for the onboarding experience is tapping on the next, previous or skip buttons, as well as all the actions that happen in the todo screen: enum OnboardingAction { case previousButtonTapped case nextButtonTapped case skipButtonTapped case placeholderAction(AppAction) }
— 2:07
We’ll also need to implement a reducer for the onboarding logic, which has the shape: let onboardingReducer = Reducer< OnboardingState, OnboardingAction, Void > { state, action, _ in … }
— 2:23
Right now we are using Void for the environment so that we can be sure that we do not accidentally invoke any of the real life dependencies.
— 2:31
This reducer will be responsible for taking us through all the steps of the onboarding experience, which currently is done in a rather ad hoc fashion by mutating some state directly in some button action closures. Let’s start by getting that functionality in the reducer: let onboardingReducer = Reducer< OnboardingState, OnboardingAction, Void > { state, action, _ in switch action { case .previousButtonTapped: state.step = state.step?.previous return .none case .nextButtonTapped: state.step = state.step?.next return .none case .skipButtonTapped: state.step = nil return .none case let .placeholderAction: return .none } }
— 3:27
We can inject this logic into our onboarding view by having the view take a store rather than an @State value: struct OnboardingView: View { // @State var step: OnboardingStep? = .actions let onboardingStore: Store<OnboardingState, OnboardingAction> … }
— 3:44
To get access to the state of this store and to send it actions in the view’s body we have to observe it, and we do that with the special WithViewStore view: var body: some View { WithViewStore(self.onboardingStore) { onboardingViewStore in … } }
— 4:12
Everything inside the WithViewStore closure has access to this onboardingViewStore object, which gives us access to the underlying state and actions of the onboarding domain. In particular, when checking if we are on an onboarding step we can reach into the view store: if let step = onboardingViewStore.step { … } else { … }
— 4:19
And when tapping a button we will no longer perform a mutation of state directly in line but instead send an action to the view store so that it can be processed by the reducer: Button(action: { onboardingViewStore.send(.previousButtonTapped) }) { … } Button("Skip") { onboardingViewStore.send(.skipButtonTapped) } Button(action: { onboardingViewStore.send(.nextButtonTapped) }) { … }
— 4:42
The WithViewStore view we are using depends on state being Equatable so that it can minimize the number of calls to its body , so we’ll want to conform OnboardingState . struct OnboardingState: Equatable { … }
— 5:26
And to get things building we need to supply this new store to our OnboardingView when we create it, such as in our preview: OnboardingView( onboardingStore: Store( initialState: .init( placeholderState: AppState(todos: .mock), step: .actions ), reducer: onboardingReducer, environment: () ), … )
— 6:14
And with those changes our onboarding feature should work exactly as it did before. We can move back and forth through the steps and various parts of the app will become redacted or unredacted, but everything is now driven off the Composable Architecture.
— 6:34
However, now that we have a proper reducer and store in place, we are free to make small tweaks to the logic. For example, suppose we want the allow the user to play with the filters control but only when on the filters step of the onboarding flow. We can switch on the nested action we want to perform and use a where clause to constrain this logic to a specific step: case let .placeholderAction(.filterPicked(filter)) where state.step == .filters: state.placeholderState.filter = filter return .none
— 7:46
This is simply performing the filter mutation directly whenever we see the filter action come through and we happen to be on the .filters step. Our preview isn’t quite ready to show this, though, because when we construct the store that we pass along to the todo application, we’re still using the empty reducer: AppView( store: Store( initialState: AppState(todos: .mock), reducer: .empty, environment: () ) )
— 8:09
Instead we want the onboardingStore to power the AppView while in the onboarding experience because it has a copy of the app state and app actions inside. So, we can use a feature of the Composable Architecture called scope to transform it and pass it along: AppView( store: self.onboardingStore.scope( state: \.placeholderState, action: OnboardingAction.placeholderAction ) // store: Store( // initialState: AppState(todos: .mock), // reducer: .empty, // environment: () // ) )
— 9:22
And this will make sure that the onboarding todo view will run off of placeholder data and logic, as we can see on the filter step of the onboarding flow!
— 9:49
This is pretty incredible. We are not only re-enabling just a subset of the logic of our feature, but we are doing it in a dynamic fashion so that the manner in which it re-enables is dependent on the context of our onboarding flow.
— 10:03
But let’s keep pushing it. Let’s also have it so that the user can check off todos when they are on the .todos step of the onboarding flow. This means we need to handle the .checkBoxToggled actions: case .placeholderAction(.todo(id: id, action: .checkBoxToggled)) where state.step == .todos: <#???#> return .none
— 10:27
And in here we need to replicate the logic we perform in the todo reducer to toggle the todo completed and sorted it down to the bottom of the list. It takes a bit of work to do it, but it’s straightforward enough: case let .placeholderAction(.todo(id: id, action: .checkBoxToggled)) where state.step == .todos: state.placeholderState.todos[id: id]?.isComplete.toggle() state.placeholderState.todos.sortCompleted() return .none
— 12:17
This basically replicates the logic, but not exactly. The real todo logic has an additional piece of logic where it does not immediately sort the todo down to the bottom when marked as completed, but instead waits a second. This allows you to complete a bunch of todos in rapid fire without having a bunch of todos flying all over the place and making it possible for you to accidentally complete the wrong todo item.
— 12:39
This logic is just complicated enough that maybe we don’t want to replicate it line for line. It would be far better if we could just call out to the live logic in here, and we can do that by invoking the appReducer directly. We will, however, need to some additional work to destructure the actions we care about before calling appReducer case let .placeholderAction(action) where state.step == .todos: switch action { case .todo(id: _, action: .checkBoxToggled), .sortCompletedTodos: return appReducer .run(&state.placeholderState, action, environment)
— 13:57
But in order to do this we need an actual environment, not just the Void one we are currently using: let onboardingReducer = Reducer< OnboardingState, OnboardingAction, AppEnvironment > { state, action, environment in … }
— 14:21
Running the appReducer returns an effect that can emit AppAction s, but we need to return an effect that can emit OnboardingAction s, so we need to further map on it in order to bundle the AppAction up into an OnboardingAction . return appReducer .run(&state.placeholderState, action, environment) .map(OnboardingAction.placeholderAction)
— 14:40
And finally, we can ignore every other action on the todos onboarding step: default: return .none
— 14:48
To get everything compiling we have to fix our preview, which can be done by providing an environment: OnboardingView( onboardingStore: Store( initialState: .init( placeholderState: AppState(todos: .mock), step: .actions ), reducer: onboardingReducer, environment: .init( analytics: .placeholder, mainQueue: DispatchQueue.main.eraseToAnyScheduler(), uuid: UUID.init ) ), … )
— 15:06
And now everything builds, and when we run the preview we see that we can’t complete todos when we are on the first two steps, but when we transition to the third step we can suddenly now complete a todo!
— 15:20
This is super cool. We were able to define a brand new onboarding reducer that can listen to all of the app actions that happen in the todo app. And we could selectively decide if we want to handle the action, ignore it, or even delegate more complicated logic to the original app reducer. Taking things even further
— 15:44
There are so many more directions we could take this concept, the possibilities are really endless. We want to describe just 3 of them before ending the episode, and we have some exercises for this episode that will help you explore even more.
— 15:58
First, we could have the user accomplish certain goals during the onboarding experience and once accomplished we can automatically transition them to the next step. For example, while looking at the filters onboarding step we could ask the user to change the filter to “Active” and then back to “All”, and once that’s done we will progress them on to the next step. Or while looking at the todos step we could ask the user to check off a few todos, and once that’s done the onboarding experience will automatically be finished and closed.
— 16:33
That is already some pretty complex logic to be adding to our application, but amazingly it would all live completely outside the main todo feature. We never have to worry about this complicated onboarding state machine code getting mixed in with the core application logic, which is complicated enough on its own. This shows just how powerful the Composable Architecture can be by allowing you to fully separate the concerns of disconnected features of your application. This kind of separation doesn’t seem easy to accomplish in vanilla SwiftUI because your logic tends to be inextricably entangled in the view that uses the logic.
— 17:18
The only thing better than being able to section off little sandboxed areas of your application’s logic so that you can power an onboarding experience is the fact that everything is still 100% testable, and can be tested exactly like how you would test any other part of your Composable Architecture application.
— 17:38
We can write a test for this onboardingReducer that automatically walks through all of the flows we’ve been doing manually in SwiftUI previews, and we can verify that certain parts of the logic execute or do not execute depending on which onboarding step we are on. We can even write an integration snapshot test that demonstrates what the UI looks like as we got through the onboarding flow. Transforming dependencies
— 17:58
And finally, it is even possible to transform dependencies in our onboarding logic before handing them off to the live application logic.
— 18:13
For example, right now we are doing the following: return appReducer .run(&state.placeholderState, action, environment) .map(OnboardingAction.app)
— 18:29
But this could be seen as a little bit dangerous to do because we are giving full, unfettered access to all of the live dependencies to the appReducer even though we are running it in the context of an onboarding experience. We’ve already seen that we can at the very least pass inert dependencies, like the placeholder analytics client we defined earlier.
— 19:13
But, what if we do want to track analytics events, but we want to slightly change the data tracked so that we can tell on our backend when an event was sent as a part of the onboarding flow or as a part of normal usage of the app. We can even accomplish this thanks to how we designed our dependencies, and it’s even something we touched upon in our last series on dependencies . We showed that if we design dependencies with simple data types instead of protocols we unlock the ability to create lots of mock behaviors in a lightweight way, and we alluded to the fact that we could even transform existing dependencies into all new ones. We even provided some exercises to explore that.
— 20:06
Well, let’s try it out here. We want to allow the onboarding appReducer to track analytics, which means giving it full access to a live analytics client: analytics: .live,
— 20:17
This can seem pretty scary to do because anything a user now does in the todo app will fire off an analytics event and there’s no way to distinguish events that happen in the onboarding flow from events that happen in the actual application.
— 20:46
What we need is a function that can transform an existing analytics client into an all new one. We can do that like this: extension AnalyticsClient { static func onboarding(_ client: Self) -> Self { .init( track: { name, properties in client.track( "[Onboarding] \(event)", properties ) } ) } } Here we modified the event name so that it as the tag "[Onboarding]" prefixed to it so that it’s clear this event was done during the onboarding phase of the application.
— 22:13
We could alternatively add a property to the event to indicate we are onboarding: client.track( "[Onboarding] \(event)", properties.merging(["onboarding": "true"], uniquingKeysWith: { $1 }) )
— 23:05
This allows us to transform any analytics client into a new one, and in particular we can transform the live client. So rather than providing the .live client directly to the onboarding appReducer , let’s transform it with the new onboarding function: analytics: .onboarding(.live),
— 23:15
This now guarantees that whatever logic the appReducer performs under the hood we can be sure that its analytics events will be properly tagged with “onboarding” in order to differentiate those events from the ones the user does in the real todo app.
— 23:48
And to reiterate, all of this is 100% testable. We can even write a test that guarantees that analytics tracked during the onboarding phase are properly tweaked to include the “onboarding” tag strings. We really want to stress the importance of this. We are pushing the architecture into all new realms that we personally had never even considered until we saw the redaction API in SwiftUI, yet we came out the other side without once compromising on understandability, maintainability or testability of our application. None of the new code we wrote is untouchable by tests. We can exercise anything and everything in a test if we wanted to, and we have some exercises to do just that.
— 25:05
And that sums up “the point” of this episode. SwiftUI has introduced a pretty incredible API that allows us to visually redact parts of our UI, and there are lots of ways to apply this tool. However, SwiftUI does not give a tool to redact the logic in our views. If you naively use the redaction API you will still have a living, breathing application running, and your users will be able to tap and interact with the UI, and potentially execute side effects using placeholder data.
— 25:36
So, we showed that the Composable Architecture gives us a really natural way to selectively redact logic in your application, all thanks to its strict separation of logic from the view that is powered by the logic. We are free to implement placeholder reducers and use placeholder stores in parts of the app so that we can control how it behaves when it is in a redacted state.
— 25:58
And we showed that this strictness with the separation even opens us up to more possibilities for cool functionality, such as dynamically adapting the application’s logic depending on what the user is doing and even testing your placeholder logic, all of which we feel is either impossible or very prohibitive to do when building a SwiftUI application in the standard, vanilla way that Apple demonstrates in their sample code.
— 26:24
Well that’s all for this series, until next time! 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 0118-redacted-swiftui-pt4 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 .