TCA: Using Composable Architecture for creating an Expandable Picker
Being on the way of working with TCA in a potentially large application, the benefits of using TCA become more and more obvious in a variety of scopes.
In this article, I’d like to share my experience in “Thinking TCA” by building a small component that presents a
Form row that can be tapped; when tapped, it expands and shows a picker for a set of values; after picking a value, the row display is updated. And the row can be collapsed, certainly…
Usually, I’d approach this by first creating an empty view, adding a
@State var for the expansion status, adding a subview for the expandable row, adding a picker to the expanded content, adding properties for the picker’s content, and adding a
@Binding var for the picker’s index. Pretty straightforward.
With TCA, I tried to convert that top-down approach to States, Actions and Reducers, and I created a pretty complicated bundle for that view. Which I didn’t like. The separation of concerns was hurt, and fixing that was not so nice.
So, I tried to think bottom-up. What is the first view we need?
The very first view I created was the
For TCA, we need a store first. The row has to states, collapsed and expanded. So a
Bool value is fully sufficient as TCA State.
What actions do we need? The Row simply reacts on a tap, so we need an action
.toggleVisibility that is triggered by
Independent of the state, we’ll present a placeholder that is an injected view; if the state is set to
true, we’ll show the injected content view.
Now, for TCA, we need an action and a reducer. How do they look like?
The TCA Action is pretty simple. The reducer is it as well, we just invert the current state (which is, you’ll remember, a
Now, we can use the view in a preview and see, if it works:
So, we’re done with that component. What next?
When the row is expanded, we want to see a picker. So, the injected
content to the
ExpandableRow view must be some kind of picker.
Here, again, we have a TCA Store, an injected placeholder view, and a
Picker with an
To fully understand the picker’s call, let’s dive into the other TCA stuff:
The picker needs a
selectedIndex, which we provide as
Int, and a list of
selectableValues, provided as array of
Because the picker shall modify the
selectedIndex, we need a binding. The TCA framework provides a special feature for the
viewStore where the current value is handed to the picker using the
get property, and the action to be triggered by the picker when changing the value is provided by the
The values are a simple loop over the
selectableValues of the
Here, again, we can check that it’s working by using a preview:
The TCA Action now has a parameter, the index provided by the picker. The reducer therefore has to react on that action by setting the state’s
selectedIndex to the provided
Obviously, you’ll see… nothing, so you may want to run the preview in debug mode and add a
debugPrint statement to the action, to see that the
selectedIndex really changes.
Again, a standalone component without any dependencies, except the injected `placeholder` view.
Now we have to combine both views, the
ExpandableRow and the
ItemPicker in order to get our final view.
This is how the view looks like:
Here again, we need a TCA store with a state and an action definition. With the
viewStore, we can then use the
ExpandableRow where the
content provides the
We now could provide the stores for the subview by using the standard initializer with
state:reducer:environment . However, if we do that, those views are completely independent: We cannot react here on changes there.
So, instead we need to use the
store.scope(state:action:). To understand this, we have a look on the remaining TCA definitions:
What we see here is that we have a
main reducer, that combines the two reducers from the subviews by a
pullback of their actions. To have this work, we need two meta actions
itemPicker with the respective subactions as parameter.
We may want to combine this with a
local reducer where we can react on whatever happens in the subviews (in this case: some
So, by using the
.scope(...) statement, we enable our reducer to access those of the subviews, and with the
.pullback operations, we can react here on what happens below.
Here again, we can check using a preview if everything’s working as expected:
This concludes our journey to another TCA application that gives us an expandable item picker that we can use itself as a component in a larger context, like we did here with the
ExpandableRow and the
ItemPicker. This makes clear, how transparent and easy large and complex application can be built using TCA, while keeping each component as isolated as possible.
I hope this is another step for your to better understand the benefits of TCA. Let me know if it was helpful for you, or if you have suggestions for more reflections.