Modular Components in Swift UI using TCA
With TCA it's possible to create small and easy-to-maintain components that can be used through an app seamlessly.
Modularity
Before we start talking about TCA it's important to reflect on what is modularity. By implementing modular design we subdivide a system into smaller parts which can be independently created, modified, replaced or exchanged with other modules or between different systems [1].
In this article, I aim to show you how you can create small and modular components with TCA.
The Composable Architecture
The Composable Architecture (TCA, for short) is a library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS) [2].
So what is TCA all about? Well to start it's necessary to get to know some types and values to help to model the domain.
State
Is the type that describes the data that your component needs to perform the logic and render the UI [3].
Action
Type that represents all of the actions that can happen in your feature. Such as user actions, notifications, events and so on [3].
Reducer
A function that describes how to evolve the current state of the component to the next state given an action. The reducer is also responsible for returning any effects that should be run and can return an Effect value [3].
Store
What drives your component. Actions are sent to the store and this store can run the reducer and its effects. You can observe state changes through the store to update the UI [3].
Example
To explain how TCA integrates everything together it would be a good idea to create a use case and build it with this framework.
Let's imagine that you want to build the following app:
Onboarding Feature
As a user, if it's my first time on the app I should see an onboarding screen.
- If I click on this onboarding screen then, when I open the app again I should see another screen.
- If I uninstall the app and install it again and open it I should see the onboarding screen again.
- If it's not my first time opening the app then I should see a screen with a list of elements.
In this Onboarding Screen, the user should see:
- A card that displays information
- When the user clicks on the card then it should be redirected to a list of elements
Even though this is very broad and you would have more details in real life we can use this example and build an app that shows a list of fruits.
My implementation of this solution was inspired by the Fruits App created by Robert Petras. You can check for a full vanilla Swift UI implementation on his website here [4].
Overview
Taking into account the requirements we can do a basic sketch of the implementation.
To start we will need a coordinator that will handle which screen to show, the Onboarding Screen or the "Normal" or List View Screen.
This coordinator will be responsible for knowing which screen to show based on the isFirstTime flag that is available in the App Storage. At any point, if the value of this flag changes we don't need to worry since it will be reflected automatically in the coordinator.
Onboarding Screen
When the Onboarding Screen is shown we need some data from the "API" to show different components. In this case, we will need a list of fruits that will be provided by the parent view in this case it will be the coordinator.
As the onboarding screen, it's only necessary to compose the list of Fruit Cards based on the list that it was received before and check for one thing:
- If in any of my child views there is a button tap then I should set the state for the isOnboarding to false, since the user already saw the onboarding screen.
Using TCA we can say that the store of the Onboarding will be a list of fruits and this screen won't have any actions, however, it will check for the actions of their children.
And it's inside the reducer that the simple logic of changing the flag to false will be done.
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .fruit(_, action: .delegate(.buttonTapped)):
// Why cant I add the app storage to the state struct?
isOnboarding = false
return .none
case .fruit(_, _):
return .none
}
}
.forEach(\.fruits, action: /Action.fruit(id:action:)){
FruitCardReducer()
}
}
Essentially, we can also create a representation of this logic with the following diagram:
The implementation of the UI of the Onboarding View could be something like this:
Fruit Card
Now inside of the fruit card, there is only one responsibility, show the information that was given and when a button is clicked send an effect of notifying that the button was tapped.
This card is not responsible for changing the App Storage value, it just sends an effect that can be handled (or not) by a parent view.
The UI implementation could be something like this:
Handling Effects
When the effect of a button tapped is sent, the onboarding reducer will be the one responsible for changing the App Storage value.
Modular Components
As you can see from this simple example, it was possible to create small components that have one responsibility and can easily interact with each other by sending effects.
Also, each of these components can be used in other places if the initialization of the state is the same. Of course, in TCA and any software implementation, we shouldn't aim to create a generic reusable element that can serve every need.
Important Note
To implement this solution I created a GitHub repository that can be accessed by everyone through this link[5]. If you have any questions about this implementation, feel free to reach out.
Thank you for taking the time to read this article.
If you enjoyed my work and want to stay updated on future projects, don't hesitate to connect with me on GitHub or LinkedIn. Thank you 🙏🏻
References
[1] Modular Design: https://en.wikipedia.org/wiki/Modular_design#:~:text=Modular%20design%2C%20or%20modularity%20in,modules%20or%20between%20different%20systems.
[2] ComposableArchitecture: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/
[3] Getting Started: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/gettingstarted
[4] Fruits App: https://learn.credo.academy/swiftui-app-fruits
[5] Fruits App in TCA: https://github.com/Sailor-Saturn/Fructus-App-TCA