이전 글에서 Tuist가 준비되었습니다. 다음은 최근 가장 유명한 TCA 입니다.
여러분이 React나 React-Native에서 Redux를 사용해봤다면, 이해하기 쉬우실 겁니다.
TCA도 역시 Reducer를 사용하기 때문이죠.
설치
설치 안내서는 Xcode를 언급합니다만, 저는 Tuist로 프로젝트를 만들었기 때문에 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)
],
이렇게 하면 1.9.2 버전을 설치하거나 패치가 있는 경우 더 상위 버전이 설치될 것 입니다.
설치된 패키지 이름은 ComposableArchitecture가 됩니다.
그리고 프로젝트를 생성하면 의존하는 많은 패키지들이 설치됩니다.

신뢰와 활성화
하지만 Swift Macro Policy 때문에 빌드는 할 수 없었습니다. 패키지에 대한 신뢰와 활성화가 필요하기 때문 입니다.

이런 오류들만 나타나서, 프로젝트를 clean하고 다시 열어보니,

경고가 생겼고 선택하면 매크로들을 활성화 할 수 있게 되어 위의 오류들이 사라졌습니다.
사용방법
Import
TCA를 사용하려면, ComposableAchitecture를 import 해야 합니다. 그런데 The는 어디 있는거죠? 🤣

Reducer
@Reducer attribute를 가지고 reducers들을 선언할 수 있습니다.
@Reducer
struct MainReducer {
}
State
Reducer는 상태를 가지고 우리는 그 상태 type을 @ObservableState attribute로 지정할 수 있습니다
@Reducer
struct MainReducer {
@ObservableState
struct State: Equatable {
var count = 0
}
}
Action
Reducer는 또한 eum으로 선언된 Action들을 가집니다. 이것들은 함수가 아닌 Type 입니다.
@Reducer
struct MainReducer {
...
enum Action {
case countIncreaseButtonTapped
case countDecreaseButtonTapped
}
}
Reduce
Reduce는 위에서 선언한 Action에 대한 실제 구현 입니다. 만약 react-redux를 사용해보셨다면 조금 이상하게 느낄 수도 있을 겁니다. redux의 reduce는 새로운 상태를 반환하기 때문이죠.
@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
상태 변경이 아닌 다른 작업을 수행하고 싶다면 .none 대신 .run를 사용하세요.
@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
다른 비동기 작업이 완료된 후 상태를 변경하고 싶을 수도 있을 겁니다. 그 때는 .run block에서 send 메소드를 실행해서 상태 변경을 위한 다른 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
SwiftUI에서 State를 사용하려면, Store 객체를 생성하세요.
let state = Store(initialState: MainReducer.State()) {
MainReducer()
}
Store르 주입하려면 StateOf로 State의 Type을 지정합니다.
let state : StoreOf<MainReducer.State>
Action
Action을 호출하려면 어떻게 해야할까요? .run block에서 했던 것 처럼 send에 넣어주면 됩니다.
state.send(.countIncreaseButtonTapped)
전체 소스
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)
}

도움이 되셨다면, 좋아요를.. 더 많은 iOS 관련글을 읽어 보세요.
저와 더 얘기하고 싶다면, LinkedIn으로 오세요. 감사합니다!
문제 해결
target CasePathsMacros must be enabled
Swift Macro들에 대해 신뢰 처리 필요

Clean하고 프로젝트를 다시 불러와서 경고를 클릭

참고자료
'Programming > iOS' 카테고리의 다른 글
기존 프로젝트에 Tuist 4 설정하기 (2) | 2025.01.17 |
---|---|
The selected Xcode version is 16.2, which is not compatible with this project’s Xcode version requirement of 14.0.0. (0) | 2025.01.12 |
Sonoma에서 Xcode 14 실행하기 #iOS #Xcode (0) | 2025.01.11 |
Ventura에서 Xcode 15 실행하는 방법 #Xcode #MacOS #강제실행 #제한해제 (0) | 2025.01.04 |