EP 116 · Redacted SwiftUI · Sep 7, 2020 ·Members

Video #116: Redacted SwiftUI: The Composable Architecture

smart_display

Loading stream…

Video #116: Redacted SwiftUI: The Composable Architecture

Episode: Video #116 Date: Sep 7, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep116-redacted-swiftui-the-composable-architecture

Episode thumbnail

Description

We’ve seen how cool redacted SwiftUI views are, but we’ve also seen some of their pitfalls: while it’s easy to redact UI, it’s not so easy to redact logic, that is unless you’re using the Composable Architecture!

Video

Cloudflare Stream video ID: 3757058f047a33a0162de7d2753bacb2 Local file: video_116_redacted-swiftui-the-composable-architecture.mp4 *(download with --video 116)*

References

Transcript

21:17

Disabling has also changed the semantic meaning of this view. It’s not really that this view is disabled, it’s just that we want to put in a static, inert version of the UI. Applying a disabled attribute might be a little too heavy handed for this situation, and could impact the user experience in ways that we are not aware of right now, such as accessibility.

0:25

Disabling may not disable everything you expect it to. It certainly disables things like buttons, text fields and other UI controls, but other events will still fire. For example, if we had an .onAppear modifier in this list so that we could fire off some analytics or an API request, that logic would still execute just like normal, even if the whole view is disabled.

0:49

Also, once you disable a view hierarchy there is no way to selectively re-enable parts of the inside of the view. This is possible with redactions. We haven’t covered it yet, but soon we will see that within a redacted view we can un-redact certain parts to make it visible to the user again. This impedance mismatch between these two APIs is really pointing at the fact that disabling UI is not a great way for handling this functionality. There may be times that we want to un-redact a little bit of the view, and inside that view we have a button. We will have no way of re-enabling it and so it will stay stuck as disabled. And what if that button did something important, like canceled the inflight request? It’s definitely not acceptable to just blanket disable everything.

1:33

So what we are seeing is that using disabled to handle this kind of placeholder functionality is not great, and so when building an application in the vanilla SwiftUI approach we may need to sprinkle bits of logic all around our view to selectively disable parts of its logic.

1:57

The main reason this is happening is that our view is inextricably linked with its logic. We have this view model object that is just plopped right down into this view, and whatever happens in the view is going to always be relayed back to the view model. This is a very tight coupling and doesn’t give us much freedom to adapt.

2:19

We feel that although SwiftUI views are an amazing technology, they are ripe for abuse in this regard. There is a phrase that people in the programming community that is very popular and seen as a kind of “north star” for software development, and it says that we should try to “separate concerns” in our applications. Now of course there is no precise definition of what “separate” and “concerns” means. Does separate mean put code in separate files, or does it mean something deeper? And can an entire screen be one concern, or must it be composed of many concerns, and how granular should we get? For this reason we think the “separation of concerns” adage is a very broad guideline, and not something that can be rigorously applied.

3:05

But, having said that, we think that SwiftUI views, when built in this kind of straightforward manner, are some of the biggest offenders of concerns not being separated. Views tend to accumulate lots of logic, especially in action closures on various UI controls, and there’s really no way in which we can say that the view and its view model are separate, isolated units. An articles app in the Composable Architecture

3:34

Well, we want to show that the Composable Architecture really embodies the spirit of “separation of concerns”, and this will lead us to some amazing results. Not only does it completely separate the logic of your feature from its view, but we are also free to plug in placeholder forms of logic, and this is the key that is missing from vanilla SwiftUI.

3:56

We will do this by looking at another version of this articles feature, except this time built using the Composable Architecture. We will see that not only is the Composable Architecture version of this application not that much more code than the vanilla SwiftUI version, but that it leaves us open to some really interesting possibilities when it comes to redacting parts of our views.

4:17

Let’s start by looking at what this articles feature looks like when built with the Composable Architecture. There really isn’t that big of a difference between it and the vanilla SwiftUI version, other than the core domain has been moved to value types and all of the logic takes place in a reducer function rather than a view model class.

4:36

It begins with the core model for the application’s domain: struct ArticlesState: Equatable { var articles: [Article] = [] var isLoading = false var readingArticle: Article? }

4:56

This struct is basically identical to what we had in our view model, except it’s a value type instead of a reference type and it doesn’t have any logic in it.

5:18

Also in the Composable Architecture we enumerate all of the actions that can take place in the UI, such as tapping an article. There are also some actions that are the result of running effects, such as articlesResponse , which is the action sent once we fetch the articles. And there are also some actions that are in a child component, which is what the article action is. It holds all the actions that can happen in a row of the list of articles, which are the following: enum ArticlesAction { case article(index: Int, action: ArticleAction) case articlesResponse([Article]?) case articleTapped(Article) case dismissArticle case onAppear } enum ArticleAction { case favoriteTapped case hideTapped case readLaterTapped }

6:05

And then we have the reducer, which is responsible for gluing together the domain to implement this feature’s business logic. It lets us evolve the current state of the application to the next state, given an action: let articlesReducer = Reducer< ArticlesState, ArticlesAction, Void > { state, action, environment in switch action { case let .article(index: index, action: .favoriteTapped): state.articles[index].isFavorite.toggle() return .none case let .article(index: index, action: .hideTapped): state.articles[index].isHidden.toggle() return .none case let .article(index: index, action: .readLaterTapped): state.articles[index].willReadLater.toggle() return .none case let .articlesResponse(articles): state.isLoading = false state.articles = articles ?? [] return .none case let .articleTapped(article): state.readingArticle = article return .none case .dismissArticle: state.readingArticle = nil return .none case .onAppear: state.isLoading = true return Effect(value: .articlesResponse(liveArticles)) .delay(for: 4, scheduler: DispatchQueue.main) .eraseToEffect() } }

6:37

Typically we would also have an environment that holds the dependencies this features needs, which allows us to perform side effects and even test them, but we’re not going to worry about that for this demo.

7:09

Then we have the main view for this feature, which has the navigation view, list, and sheet for displaying an article: struct ComposableArticlesView: View { let store: Store<ArticlesState, ArticlesAction> var body: some View { WithViewStore(self.store) { viewStore in NavigationView { List { if viewStore.isLoading { ActivityIndicator() .padding() .frame(maxWidth: .infinity) } ArticlesListView(store: self.store) } .sheet( item: viewStore.binding( get: \.readingArticle, send: .dismissArticle ) ) { article in NavigationView { ArticleDetailView(article: article) .navigationTitle(article.title) } } .navigationTitle("Articles") } .onAppear { viewStore.send(.onAppear) } } } }

7:34

We are delegating the rendering of the contents of the list to a secondary view just because things were getting a little nested. It is true that using the Composable Architecture can indent your views by one additional level due to the use of WithViewStore . Alternatively we could also hold onto the view store as an @ObservableObject to get rid of this indentation.

9:03

Here’s what the view looks like that is responsible for just displaying the list of articles and nothing else: struct ArticlesListView: View { let store: Store<ArticlesState, ArticlesAction> var body: some View { WithViewStore(self.store) { viewStore in ForEachStore( self.store.scope( state: \.articles, action: ArticlesAction.article ) ) { articleStore in WithViewStore(articleStore) { articleViewStore in Button(action: { viewStore.send(.articleTapped(articleViewStore.state)) }) { ArticleRowView(store: articleStore) } .buttonStyle(PlainButtonStyle()) } } } } }

9:29

And then finally we have the view that renders each row of the list, which basically looks the same as the vanilla SwiftUI view: private struct ArticleRowView: View { let store: Store<Article, ArticleAction> var body: some View { WithViewStore(self.store) { viewStore in HStack(alignment: .top) { Image("") .frame(width: 80, height: 80) .background(Color(white: 0.9)) .padding([.trailing]) VStack(alignment: .leading) { Text(viewStore.title) .font(.title) Text(articleDateFormatter.string(from: viewStore.date)) .bold() Text(viewStore.blurb) .padding(.top, 6) HStack { Spacer() Button(action: { viewStore.send(.favoriteTapped) }) { Image(systemName: "star.fill") } .buttonStyle(PlainButtonStyle()) .foregroundColor(viewStore.isFavorite ? .red : .blue) .padding() Button(action: { viewStore.send(.readLaterTapped) }) { Image(systemName: "book.fill") } .buttonStyle(PlainButtonStyle()) .foregroundColor(viewStore.willReadLater ? .yellow : .blue) .padding() Button(action: { viewStore.send(.hideTapped) }) { Image(systemName: "eye.slash.fill") } .buttonStyle(PlainButtonStyle()) .foregroundColor(.blue) .padding() } } } .padding([.top, .bottom]) .buttonStyle(PlainButtonStyle()) } } } private struct ArticleDetailView: View { let article: Article var body: some View { Text(self.article.blurb) } }

9:51

And with that we have basically recreated the articles feature, but in the Composable Architecture. If you’ve ever built anything in the Composable Architecture then you know just how powerful this style of building can be, and if you are new to the architecture then you should know:

10:13

All of the application’s logic is in a single composable unit: the reducer. We even have the logic that governs a single article row in this reducer, which means we are able to implement that hiding feature we had problems with before very easily.

11:01

Testing this feature is now trivial: as easy as testing a function. We even have testing helpers that make it super ergonomic to write tests against business logic that exercise the full end-to-end lifecycle of effects, and print out test failures in a really nice format that shows line-by-line why the assertion failed.

11:23

And most importantly we now have a very high level understanding of how data flows through our application. The only time changes are made in our application is when the reducer processes an action and mutates state. This gives us a single place to control our feature’s logic. We can even inspect how actions are flowing into the system and make tweaks to those actions before they continue through the system. We’ll have more to say about that soon. Redacting in the Composable Architecture

11:50

Now the question is, how do we get placeholder articles and redactions in the view when it’s loading. Well, redacting content based on the isLoading state goes mostly the same as it did for the vanilla SwiftUI application: ArticlesListView( store: self.store ) .redacted(reason: viewStore.isLoading ? .placeholder : [])

12:30

But how do we get some placeholder content showing?

12:35

We could add some logic to our reducer to put in some placeholder articles while loading: case .onAppear: state.isLoading = true state.articles = placeholderArticles return Effect(value: .articlesResponse(liveArticles)) .delay(for: 4, scheduler: DispatchQueue.main) .eraseToEffect()

13:05

And now the view seems to work as we expect, it shows some redacted placeholders while loading and then the content pops in. However, it has the bad behavior that we’ve seen before where the UI is still fully functional.

13:36

To fix this we could repeat techniques we saw before, such as adding additional logic to the reducer to short circuit business logic, or we could just disable the whole list and be done with it.

14:24

But because we are using the Composable Architecture, and because all of this feature’s core logic has been completely isolated from the view, there is something much much cooler we can do.

14:34

Right now we are handing the store on down to the ArticleListView here: ArticlesListView( store: self.store )

14:39

This store holds all of the logic for this feature. It evolves the state over time as actions occur and it processes side effects and feeds their data back into the system. This is very cool, and by passing this store down to the child view it means any changes the child makes to the domain will be instantly reflected in the parent.

15:04

However, there’s nothing stopping us from providing a different store to this view. When we construct a store we must specify 3 things: the initial state of the application, the reducer that runs the applications logic, and the environment dependencies the application needs to do its job. What if we constructed a whole new store with whatever state we wanted to show for the placeholder rows, a reducer that does literally nothing, and an environment with no dependencies?

15:40

That could look something like this: ArticlesListView( store: viewStore.isLoading ? Store( initialState: .init(articles: placeholderArticles), reducer: .empty, environment: () ) : self.store ) .redacted(reason: viewStore.isLoading ? .placeholder : [])

16:45

This provides a custom store whenever the articles are loading, and when not loading we pass along the real store.

17:00

And we’ll also delete the placeholder logic from our reducers: case .onAppear: state.isLoading = true // state.articles = placeholderArticles return Effect(value: .articlesResponse(liveArticles)) .delay(for: 4, scheduler: DispatchQueue.main) .eraseToEffect()

17:07

Now when we run the application we will see it behaves as before, except now we can click on anything we want and nothing happens. We have basically swapped out all of the logic that powers the articles list view with a placeholder store that is incapable of executing any logic. All in a single line of code.

17:36

We have essentially created a store that is completely inert and never changing. On the outside it looks like a perfectly fine store. It even has all the right generics and everything, which is why we are allowed to pass it along to the ArticlesListView . But on the inside it will never change, no matter what action you send it. Even better, because it has no dependencies we know for a fact that it can never execute any side effects. So it will never fire off an API request or track analytics for placeholder data.

18:23

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?

18:55

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.

19:19

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?

19:41

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.

20:10

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…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 0116-redacted-swiftui-pt2 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 .