Swift/Swift CS

[iOS/TCA] TwoCounters로 TCA Scope알아보기

힛해 2024. 8. 30. 17:05
728x90

소스코드 개요

더보기

이 화면에서는 \를 사용하여 작은 기능을 사용하여 더 큰 기능으로 구성하는 방법을 보여 줍니다
리듀서 빌더와 'Scope' 리듀서, 매장의 'Scope' 오퍼레이터.

카운터 화면의 도메인을 재사용하여 두 번 더 큰 도메인에 임베딩합니다

 

전체 코드

@Reducer
struct TwoCounters {
  @ObservableState
  struct State: Equatable {
    var counter5 = Counter.State()
    var counter2 = Counter.State()
  }

  enum Action {
    case counter1(Counter.Action)
    case counter2(Counter.Action)
  }

  var body: some Reducer<State, Action> {
    Scope(state: \.counter5, action: \.counter5) {
        // counter1의 상태와 counter1의 액션을 CounterReducer로 조작
      Counter()
    }
    Scope(state: \.counter2, action: \.counter2) {
      Counter()
    }
  }
}

struct TwoCountersView: View {
  let store: StoreOf<TwoCounters>

  var body: some View {
    Form {
      Section {
        AboutView(readMe: readMe)
      }

      HStack {
        Text("Counter 1")
        Spacer()
        CounterView(store: store.scope(state: \.counter1, action: \.counter1))
      }

      HStack {
        Text("Counter 2")
        Spacer()
        CounterView(store: store.scope(state: \.counter2, action: \.counter2))
      }
    }
    .buttonStyle(.borderless)
    .navigationTitle("Two counters demo")
  }
}

 

Counter 리듀서

@Reducer
struct Counter {
  @ObservableState
  struct State: Equatable {
    var count = 0
  }

  enum Action {
    case decrementButtonTapped
    case incrementButtonTapped
  }

  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      case .decrementButtonTapped:
        state.count -= 1
        return .none
      case .incrementButtonTapped:
        state.count += 1
        return .none
      }
    }
  }
}

 

같은 동작을 하지만 다른 상태관리가 필요한 경우 상위 리듀서에 중복으로 선언해 각기 다른 상태관리를 할 수 있다.

 

리듀서 선언 방식

var body: some Reducer<State, Action> {
    Scope(state: \.counter5, action: \.counter5) {
        // counter1의 상태와 counter1의 액션을 CounterReducer로 조작
      Counter()
    }
    Scope(state: \.counter2, action: \.counter2) {
      Counter()
    }
  }

 

View 사용 방식

HStack {
        Text("Counter 2")
        Spacer()
        CounterView(store: store.scope(state: \.counter2, action: \.counter2))
      }
      
...

struct CounterView: View {
  let store: StoreOf<Counter>

  var body: some View {
    HStack {
      Button {
        store.send(.decrementButtonTapped)
      } label: {
        Image(systemName: "minus")
      }

      Text("\(store.count)")
        .monospacedDigit()

      Button {
        store.send(.incrementButtonTapped)
      } label: {
        Image(systemName: "plus")
      }
    }
  }
}

 

store.scope는 리듀서에 선언한 Scope에 접근할 수 있게 하는 키워드이고

 

\. Key Path 문법을 사용해 counter1 속성에 접근해 이 속성을 함수에 전달해서 사용한다.

 

만약 이런 문법을 사용하지 않았다면 store.State.counter1 로 사용했어야했다.

 

 

\.counter5와 같은 Key Path는 구조체 State 내의 특정 속성(counter5)에 접근하기 위한 경로를 제공한다.

이것을 통해 Scope와 같은 함수나 메서드가 해당 속성에 쉽게 접근할 수 있도록 해준다.

1. 변수 선언과 Key Path

@ObservableState
struct State: Equatable {
    var counter5 = Counter.State()
    var counter2 = Counter.State()
}

 

위 코드에서 State 구조체에는 counter5와 counter2라는 두 개의 속성이 있고, 각각은 Counter.State 타입을 가지고 있다.

2. Key Path를 사용하는 Scope

Scope(state: \.counter5, action: \.counter1) {
    Counter()
}
Scope(state: \.counter2, action: \.counter2) {
    Counter()
}
  • Key Path \.counter5와 \.counter2는 State 구조체 내의 counter5와 counter2 속성에 접근합니다.
  • Scope는 Key Path를 사용하여 State의 특정 속성을 추적하고, 이를 기반으로 하위 상태(Counter.State)와 액션을 처리합니다.

3. 어떻게 접근이 가능한가?

  • Scope(state: \.counter5, action: \.counter1)에서 state: \.counter5는 State 구조체의 counter5 속성을 가져오고, 이를 Counter 리듀서에 연결합니다.
  • 이처럼 Key Path는 Scope가 어떤 상태와 액션에 연결되어야 하는지를 정의하는 데 사용됩니다.

4. 예시 코드 분석

Scope(state: \.counter5, action: \.counter1) {
    Counter()
}
 
  • 여기서 state: \.counter5는 State 구조체 내의 counter5 속성을 참조합니다.
  • 그러므로, Scope는 Counter 리듀서와 counter5 상태를 연결하게 됩니다.
  • action: \.counter1는 Action 열거형 내의 counter1 케이스와 연결됩니다. 즉, counter1 액션이 발생하면 Counter 리듀서가 처리하게 됩니다.

결론

  • Key Path는 특정 상태에 접근할 수 있는 경로를 제공하는 방식입니다.
  • Scope 함수 내에서 Key Path를 사용해 State 구조체의 특정 속성(counter5 또는 counter2)을 선택하고, 그것을 하위 리듀서(여기서는 Counter)와 연결합니다.
 
더보기

\.은 Key Path를 나타내는 Swift 문법입니다. Key Path는 특정 구조체나 클래스의 속성에 접근할 때 사용됩니다.

Scope(state: \.counter1, action: \.counter1)

 

위 코드에서 \.counter1은 TwoCounters.State 구조체의 counter1 속성에 접근하는 Key Path입니다.

Key Path의 용도:

  • Key Path는 구조체나 클래스의 특정 속성에 접근하거나, 그 속성을 다른 함수에 전달할 때 사용됩니다.
  • \.counter1은 state.counter1처럼 state 객체에서 counter1 속성에 접근하는 방법과 유사하지만, 함수나 메서드에 경로로서 전달될 수 있다는 점에서 차이가 있습니다.

Swift에서의 Key Path 사용:

Key Path는 다음과 같이 사용됩니다:

  • \Type.property: 특정 타입의 속성에 대한 경로를 나타냅니다.
  • \.property: Key Path가 이미 어떤 타입의 속성을 참조하고 있는 컨텍스트에서 사용됩니다. (타입은 생략 가능)

코드 내 사용 설명:

  • Scope(state: \.counter1, action: \.counter1)에서 \.counter1은 TwoCounters.State에서 counter1 상태에 접근하는 방법을 정의합니다.
  • 이 Key Path를 통해 Scope는 counter1 상태에 직접 접근하고, 해당 상태에 관련된 액션을 처리합니다.