Setting up TCA for your project with Tuist
Tuist is ready in previous article. Next is TCA. TCA is also one of famous tools.
If you used Redux in React/React-Native, the concept might be easy for you.
Because TCA has Reducer.
Installation
The Installation Guide mentioned Xcode, however we created project with Tuist. Therefore I appended to packages.
let project = Project(
...,
packages: [
...,
.remote(
url: "https://github.com/pointfreeco/swift-composable-architecture",
requirement: .upToNextMinor(from: "1.9.2"))
],
targets: [
.target(
...,
dependencies: [
...,
.package(product: "ComposableArchitecture", type: .runtime)
],
This will install 1.9.2 and upgrade if there is any fix. The installed package name is ComposableArchitecture.
After generating project, many dependency packages also installed.
Trust & Enable
But I couldn’t build due to Swift Macro Policy. They need to Trust and Enable.
There is only errors, so I cleaned the project and opened Xcode.
By clicking the warnings, it was possible to enable macros and the errors have been solved.
Usage
Import
To use TCA, I imported ComposableAchitecture
. Where is The
? 🤣
Reducer
With @Reducer attribute, we can define reducers.
@Reducer
struct MainReducer {
}
State
Reducer can have the state and we can specify the state type with @ObservableState attribute.
@Reducer
struct MainReducer {
@ObservableState
struct State: Equatable {
var count = 0
}
}
Action
Reducer has also actions declared as an enumeration. These are action type, not action function.
@Reducer
struct MainReducer {
...
enum Action {
case countIncreaseButtonTapped
case countDecreaseButtonTapped
}
}
Reduce
Reduce is the real implementation of actions. If you used react-redux, this is weird, because the redux’s reduce returns a new state.
@Reducer
struct MainReducer {
...
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .countIncreaseButtonTapped:
state.count += 1
return .none
case .countDecreaseButtonTapped:
state.count -= 1
return .none
}
}
}
}
run
If you want to run something different task, not to change a state. use .run
instead of .none
.
@Reducer
struct MainReducer {
enum Action {
...
case uploadCount
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
...
case .uploadCount:
return .run { send in
// await task...
}
}
}
}
}
send
In other case, we need to update state when finished asynchronous tasks. By call send
method in .run
block, we can update another action.
@Reducer
struct MainReducer {
@ObservableState
struct State: Equatable {
...
var uploadingResult = ""
}
enum Action {
...
case uploadCount
case updateUploadingResult(result: String)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
...
case .uploadCount:
return .run { send in
// await task...
await send(.updateUploadingResult(result: "completed"))
}
case .updateUploadingResult(result: let result):
state.uploadingResult = result
return .none
}
}
}
}
SwiftUI
state
To use the state in SwiftUI, create state instance.
let state = Store(initialState: MainReducer.State()) {
MainReducer()
}
To inject store, specify store type with StateOf.
let state : StoreOf<MainReducer.State>
action
So how to call actions? Just call send
like .run
block.
state.send(.countIncreaseButtonTapped)
Full Source
import SwiftUI
import SwiftData
import ComposableArchitecture
@Reducer
struct MainReducer {
@ObservableState
struct State: Equatable {
var count = 0
var uploadingResult = ""
}
enum Action {
case countIncreaseButtonTapped
case countDecreaseButtonTapped
case uploadCount
case updateUploadingResult(result: String)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .countIncreaseButtonTapped:
state.count += 1
return .none
case .countDecreaseButtonTapped:
state.count -= 1
return .none
case .uploadCount:
return .run { send in
// await task...
await send(.updateUploadingResult(result: "completed"))
}
case .updateUploadingResult(result: let result):
state.uploadingResult = result
return .none
}
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
let state = Store(initialState: MainReducer.State()) {
MainReducer()
}
var body: some View {
NavigationSplitView {
Text("\(state.count)")
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
} detail: {
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(timestamp: Date())
modelContext.insert(newItem)
state.send(.countIncreaseButtonTapped)
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
state.send(.countDecreaseButtonTapped)
}
}
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
}
If you found this post helpful, please give it a round of applause 👏. Explore more iOS-related content in my other posts.
For additional insights and updates, check out my LinkedIn profile. Thank you for your support!
Troubleshootings
target CasePathsMacros must be enabled
Swift Macros are required to be trusted
Clean and reload and Click warning