swift

The Composable Architecture(TCA)

motosw3600 2022. 3. 2. 13:59

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