Photo by Ricardo Gomez Angel on Unsplash

The Composable Architecture (TCA) in a Nutshell

Uwais Alqadri
4 min readOct 22, 2023

--

The Composable Architecture (TCA) is an architecture that was developed by Point Free and is meant to be compatible with Declarative UI especially SwiftUI.

TCA itself adopts quite an amount of the concept from React-Redux, each view will have a Reducer that holds the State and Action, and this is actually quite a phenomenon we’ve seen in modern mobile app development where every single architectural pattern will have sort of mechanism that copies React-Redux to support Unidirectional Data Flow (e.g Flutter BLOC, Compose MVI, SwiftUI TCA)

What is a Reducer?

Think of a Reducer as what defines a View, below image shows that HomeView will have the following State and Action

As the popular quote says “Code that Explains Itself”, Reducer defines what happens inside a view, to get you into this, let's see the following code

 public struct State: Equatable {
public var list: [Giphy] = []
public var errorMessage: String = ""
public var isLoading: Bool = false
public var isError: Bool = false
}

public enum Action {
case fetch(request: String)
case removeFavorite(item: Giphy, request: String)

case success(response: [Giphy])
case failed(error: Error)
}
public struct FavoriteReducer: Reducer {

private let useCase: FavoriteInteractor
private let removeUseCase: RemoveFavoriteInteractor

init(useCase: FavoriteInteractor, removeUseCase: RemoveFavoriteInteractor) {
self.useCase = useCase
self.removeUseCase = removeUseCase
}

public var body: some ReducerOf<Self> {
Reduce<State, Action> { state, action in
switch action {
case .fetch(let request):
state.isLoading = true
return .run { send in
do {
let response = try await self.useCase.execute(request: request)
await send(.success(response: response))
} catch {
await send(.failed(error: error))
}
}

case .success(let data):
state.list = data
state.isLoading = false
return .none

case .failed:
state.isError = true
state.isLoading = false
return .none

case .removeFavorite(let item, let request):
return .run { send in
do {
let response = try await self.removeUseCase.execute(request: item)
await send(.fetch(request: request))
} catch {
await send(.failed(error: error))
}
}

}
}
}
}

As you can see, the code above clearly tells us what happened inside FavoriteView, it tells:

  • The correlation between one action to another
  • When this-and-that state happens
  • What logic that triggered by this action, what state it produces

In TCA, Reducer is the layer that represents View (or Root View)

Why it’s called “Composable”?

Before we get into the TCA, I’ll address the elephant in the room for you

Before the era of Declarative UI, we looked at a View as a View, and Screen (UIViewController, Activity, Fragment) as a Screen, the problem is that such a thing is not what Declarative UI is.

In Declarative UI, whether the View is presented or pushed, it’s still a View hierarchy as it is

Both might seem different, the left diagram shows navigation between screens, and the right diagram shows a connection between views and their subviews, but in declarative UI, both are the same thing, It’s just a View to another View.

for TCA, not only do we compose the views, we also compose reducers.

struct MainTabReducer: Reducer {
struct State: Equatable {
var homeTab = AppCoordinator.State.rootHomeState
var searchTab = AppCoordinator.State.rootSearchState
var selectedTab: Tabs = .home
}

enum Action {
case homeTab(AppCoordinator.Action)
case searchTab(AppCoordinator.Action)
case selectedTabChanged(Tabs)
}

var body: some ReducerOf<Self> {
Reduce<State, Action> { state, action in
switch action {
case let .selectedTabChanged(tab):
state.selectedTab = tab
return .none

case .homeTab, .searchTab:
return .none
}
}
Scope(state: \.homeTab, action: /Action.homeTab) {
AppCoordinator()
}
Scope(state: \.searchTab, action: /Action.searchTab) {
AppCoordinator()
}
}
struct MainTabView: View {
let store: StoreOf<MainTabReducer>

var body: some View {
WithViewStore(store, observe: \.selectedTab) { viewStore in
NavigationView {
ZStack {
switch viewStore.state {
case .home:
AppCoordinatorView(
coordinator: store.scope(
state: \.homeTab,
action: { .homeTab($0) }
)
)
case .search:
AppCoordinatorView(
coordinator: store.scope(
state: \.searchTab,
action: { .searchTab($0) }
)
)
}

. . .

The MainTabView will show either HomeView or SearchView, so it means we also compose the Reducer and connect from one Reducer to another by with Store Scope

Closing

Please read and watch learning materials from the developer Point Free in their course for more in-depth knowledge
https://www.pointfree.co/collections/composable-architecture

Also please check out my Repository that I use as a code sample in this article
https://github.com/uwaisalqadri/GiphyGIF

Hopefully, that covers and explains correctly what the developer intends for it, It’s me Uwais, P E A C E O U T!

--

--

Uwais Alqadri

A person who's curious about Mobile Technology and very passionate about it. specialize in Swift (Apple Platforms) and Kotlin (Android, Kotlin Multiplatform).