The Composable Architecture(TCA)
- pointfree에서 Brandon Williams와 Stephen Ceils가 만들어낸 아키텍처
- 상태관리, Composition, Side Effect, Testing하게 설계할 수 있는 아키텍처
- SwiftUI, UIKit을 지원하고 다른 Apple Flatform(macOS, tvOS, watchOS)에서 사용가능
- 내부적으로 Combine으로 구성되어 있음(Store...)
- SPM으로 설치
Composable Architecture 구성 이해하기
- State: 로직을 수행하고 UI를 렌더링을 수행하기 위해 필요한 데이터 타입
- Action: 사용자 작업, 알림, 이벤트 소스등 기능에서 발생할 수 있는 모든 타입
- Environment: API clients, analytics client등등 사이드 이펙트를 동반하는 모든 디펜던시들을 가지고 있는 타입
- Reducer: Action이 주어지면 앱의 현재 상태를 다음 State로 업데이트 하는 방법을 설명하는 함수, API요청 과 같이 실행해야 하는 모든 결과를 Effect타입으로 반환해야 함
- Store: 기능을 실제로 실행하는 런타임. 모든 사용자 작업을 Store로 전송하여 Store에서 Reducer와 Effect를 실행 State변화를 관찰하여 UI를 업데이트
TCA 이점
- 별도의 기능을 함께 구성함으로써 기능의 테스터블함이 가능하고, 크고 복잡한 기능들을 작은 도메인으로 나눌 수 있다.
- 다른 구성요소를 통한 데이터 흐름은 명확하게 정의되고 단방향으로 쉽게 이해가능하다.
- Environment에는 모든 dependency들이 포함된다. 한곳에서 외부 의존성을 관리할 수 있다.
- Reducer에서 작업을 처리하여 상태를 변환(ReactorKit에서의 mutate, reduce한번에 처리)
ComposableArchitecture 예제
간단한 - + 버튼으로 count하는 View를 ComposableArchitecture로 구성해보자
State
struct AppState: Equatable {
var count = 0
var numberFactAlert: String?
}
값의 상태값을같는 count, Alert창의 문구 numberFactAlert
Action
enum AppAction: Equatable {
case factAlertDismissed
case decrementButtonTapped
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(Result<String, ApiError>)
}
Action에따른 각각의 eventcase를 선언
Environment, ApiError
struct AppEnvironment {
var mainQueue: AnySchedulerOf<DispatchQueue>
var numberFact: (Int) -> Effect<String, ApiError>
}
struct ApiError: Error, Equatable { }
Api를 수신받을 mainQueue, 의존성 Int를 받아 Event<String, ApiError>를 반환하는 함수 선언
String은 response를 요약한값
Reducer
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
switch action {
case .factAlertDismissed:
state.numberFactAlert = nil
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
case .numberFactButtonTapped:
return environment.numberFact(state.count)
.receive(on: environment.mainQueue)
.catchToEffect()
.map(AppAction.numberFactResponse)
case let .numberFactResponse(.success(fact)):
state.numberFactAlert = fact
return .none
case .numberFactResponse(.failure):
state.numberFactAlert = "Could not load a number fact"
return .none
}
}
action에 따른 state변화와 어떤 Effect를 실행할지 정의
어떠한 Effect도 실행이 필요하지 않은 경우는 .none 리턴
View구성(SwiftUI)
struct AppView: View {
let store: Store<AppState, AppAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
HStack {
Button("−") { viewStore.send(.decrementButtonTapped) }
Text("\(viewStore.count)")
Button("+") { viewStore.send(.incrementButtonTapped) }
}
Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
}
.alert(
item: viewStore.binding(
get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
send: .factAlertDismissed
),
content: { Alert(title: Text($0.title)) }
)
}
}
}
struct FactAlert: Identifiable {
var title: String
var id: String { self.title }
}
※ UIKit으로도 viewDidLoad에서 바인딩을 걸어 적용할 수 있음
출처
https://github.com/pointfreeco/swift-composable-architecture
'swift' 카테고리의 다른 글
RxSwift UnitTest 해보기(RxTest, RxNimble) (0) | 2022.03.10 |
---|---|
Tuist로 프로젝트 관리해보기 (0) | 2022.03.05 |
Memory Debugging(leak Test) (0) | 2022.02.20 |
DispatchSemaphore (0) | 2022.02.06 |
UICollectionView Custom Layout (0) | 2022.02.03 |