TCA(The Composable Architecture) with SwiftUI — The Unidirectional flow.

Lakshminaidu C
4 min readDec 9, 2023

--

The Composable Architecture (TCA) is a modern architecture framework primarily used in iOS application development, popularized by the Swift community. Created from Point-Free (https://www.pointfree.co/), it’s based on the principles of functional programming and reactive programming.

Here are key points about TCA:

  1. State Management: TCA focuses on unidirectional data flow. The entire state of the application is stored in a single source of truth. This ensures predictability and enables easier testing.
  2. State Changes: State mutations are performed via actions. Actions are simple data structures that represent user interactions or system events. They are processed by reducers, which are pure functions that take the current state and an action as input and produce a new state.
  3. Store: TCA introduces the concept of a store, which manages the state and handles the actions, dispatching them to the appropriate reducers.
  4. View State and Side Effects: Views in TCA are driven by the state. They only display the current state and send actions in response to user interactions. Side effects, such as network requests or database operations, are handled using the Effect type, keeping the reducers pure.
  5. Modularity and Composition: TCA encourages the decomposition of complex systems into smaller, composable parts. This enables easy reuse and testing of components.
  6. Testing: TCA’s emphasis on pure functions and unidirectional data flow simplifies testing. Components can be tested in isolation, and the behavior of the application can be easily predicted.
  7. Debugging: The unidirectional flow and immutability make debugging easier since the state changes are more predictable and traceable.
  8. Swift and SwiftUI: TCA is particularly well-suited for SwiftUI applications due to its compatibility with Swift language features and SwiftUI’s declarative nature.
  9. Learning Curve: While TCA offers significant advantages, it might have a steeper learning curve for developers new to functional programming concepts or unidirectional architectures.
  10. Community Support: TCA has gained a growing community of developers who contribute resources, libraries, and discussions, making it easier for newcomers to learn and adopt.

Overall, TCA provides a structured and scalable approach to building iOS applications, emphasizing predictability, testability, and maintainability. It’s not just a framework but a set of principles that can be applied to various architectures beyond iOS development.

let’s dive into code

Composable Architecture introduces three primary components: State, Action, Store and Reducer.

  1. State: This is a single source of truth. The State contains all the data your view needs to render itself.
  2. Action: Actions are events that can mutate the state or perform side-effects like API calls.
  3. Reducer: A Reducer is a pure function that describes how to evolve the current state to the next state given an action.
  4. Environment (Optional) — This is for any dependencies used, for example, a networking module.
  5. Store — The store manages all of the above. It receives an action and runs the reducer to modify the state.
  6. Effect — An event that modifies the state outside of its local context.

To use the Composable Architecture framework, you need to install it into your project. To install the SDK through Swift Package Manager:

Sample Project:

Its a CryptoApp, having three features

  1. Fetch the data from Api
  2. search the coins
  3. Sort the coins by filter

let’s create state, action and store


@Reducer
struct CoinFeature {
@ObservableState
struct State: Equatable {
public static func == (lhs: CoinFeature.State, rhs: CoinFeature.State) -> Bool {
lhs.id == rhs.id
}
private var id: UUID { UUID() }
var filterOption: String = "ALL"
var searchText: String = ""
var isLoading: Bool = true
var initialCoinData: [CoinData] = []
var coinData: [CoinData] = []
}

public enum Action: BindableAction {
case binding(BindingAction<State>) // bindiang state changes
case onAppear // on appear
case processAPIResponse([CoinData]) // process api data
case updateCoinData([CoinData]) // update data on user actions(search and filter change)
}
@Dependency(\.apiClient) var apiClient // fetch data
var body: some ReducerOf<Self> {
BindingReducer() // binds state changes
Reduce { state, action in
switch action {
case .onAppear:
state.isLoading = true
return .run { send in
do {
let coinDataCopy: [CoinData] = try await apiClient.request(endPoint: CoinRatesEndPoint.coinRates)
await send(.processAPIResponse(coinDataCopy))
} catch {
}
}
case let .processAPIResponse(data):
state.isLoading = false
state.initialCoinData = data.sorted {$0.cmcRank > $1.cmcRank}
state.coinData = state.initialCoinData
return .none
case let .updateCoinData(data):
state.coinData = data
return .none
case .binding(\.searchText):
return .run { [state = state] send in
if state.searchText.isEmpty {
await send(.updateCoinData(state.initialCoinData))
} else {
await send(.updateCoinData(state.coinData.filter { $0.symbol.localizedCaseInsensitiveContains(state.searchText)}))
}
}
case .binding(\.filterOption): // filter change
return .run { [state = state] send in
switch state.filterOption {
case FilterOption.price.rawValue:
return await send(.updateCoinData(state.coinData.sorted {$0.quote["USD"]!.price > $1.quote["USD"]!.price}))
case FilterOption.volume24h.rawValue:
return await send(.updateCoinData(state.coinData.sorted {$0.quote["USD"]!.volume24H > $1.quote["USD"]!.volume24H}))
default:
return await send(.updateCoinData(state.coinData.sorted {$0.cmcRank > $1.cmcRank}))
}
}
default: return .none
}
}
}
}

enum FilterOption: String, CaseIterable {
case price = "Price"
case volume24h = "24H Volume"
case all = "ALL"
}

Lets create View

import SwiftUI
import ComposableArchitecture

struct MainApp: App {
var body: some Scene {
WindowGroup {
CryptoFeatureView(store: .init(initialState: .init(), reducer: {
CryptoFeature()
}))
}
}
}

struct CoinFeatureView: View {
@State var store: StoreOf<CoinFeature>
var body: some View {
ZStack {
VStack {
HeaderView()
SearchSubHeaderView(searchTxt: $store.searchText,
selectedValue: $store.filterOption
)
.padding(EdgeInsets(top: 20, leading: 10, bottom: 10, trailing: 10))
CryptoList(coinDataList: store.coinData, cryptoIcons: [:])
}
.overlay {
VStack {
Spacer()
TabbarView()
.overlay {
IconBtn(buttonIcon: TabBarOption.metaverse.icon) {
}
.offset(y: -20)
}
}
}
if store.isLoading {
LoadingView()
}
}
.onAppear {
store.send(.onAppear)
}
}
}

Source code

If you would like to view the source code you can here https://github.com/lakshminaidu/TCA-GIOSTASK

Thanks for reading it.

Please follow me Lakshminaidu for more updates.

--

--