ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TCA] TCA 작동 방식
    iOS/TCA 2023. 8. 31. 15:04

     

    ✅ TCA 작동방식에 대해 정리하고 가자!

     

     

     

     

     

    TCA 구조의 특징

    로직이 한 방향으로 흐르기 때문에 각 타입의 역할을 이해하면 side-effect 관리를 쉽게 할 수 있다

     


     

    역할

    State

    상태값을 저장하는 객체

     

    Action

    사용자의 동작이 일어났을 때 참조하는 객체

     

    Store

    State, Action, Reducer가 모두 저장되어 있는 Observable 오브젝트

    ex) 사용자가 버튼을 클릭하면 -> action에서 reducer로 전달 -> state값 변경

     

    Reducer

    들어오는 action을 필터링해서 state 값을 변경할 수 있는 함수

     


    PrimeTime 코드 예시

    State

    struct AppState {
        var count = 0
        var favoritePrimes: [Int] = []
        var loggedInUser: User?
        var activityFeed: [Activity] = []
        
        var didChange = PassthroughSubject<Void, Never>()
        
        struct Activity {
            let timestamp: Date
            let type: ActivityType
            
            enum ActivityType {
                case addedFavoritePrime(Int)
                case removedFavoritePrime(Int)
            }
        }
        
        struct User {
            let id: Int
            let name: String
            let bio: String
        }
    }

     

    Action

    public enum CounterAction {
        case decrementTapped
        case incrementTapped
    }
    
    
    public func counterReducer(state: inout Int, action: CounterAction) -> Void {
        switch action {
        case .decrementTapped:
            state -= 1
            
        case .incrementTapped:
            state += 1
        }
    }
    
    // ========================================
    
    enum AppAction {
        case counter(CounterAction)
        case primeModal(PrimeModalAction)
        case favoritePrimes(FavoritePrimesAction)
        
        // enum properties
        // 프로퍼티를 사용하는 경우 AppAction 열거형에 있는 모든 케이스의 관련 데이터에 대한 인스턴스 액세스 가능
        var counter: CounterAction? {
            get {
                guard case let .counter(value) = self else { return nil }
                return value
            }
            set {
                guard case .counter = self, let newValue = newValue else { return }
                self = .counter(newValue)
              }
        }
        var primeModal: PrimeModalAction? {
            get {
                guard case let .primeModal(value) = self else { return nil }
                return value
            }
            set {
                guard case .primeModal = self, let newValue = newValue else { return }
                self = .primeModal(newValue)
            }
        }
        var favoritePrimes: FavoritePrimesAction? {
            get {
                guard case let .favoritePrimes(value) = self else { return nil }
                return value
            }
            set {
                guard case .favoritePrimes = self, let newValue = newValue else { return }
                self = .favoritePrimes(newValue)
            }
        }
    }

     

    Reducer

    let _appReducer: (inout AppState, AppAction) -> Void = combine(
        pullback(counterReducer, value: \.count, action: \.counter),
        pullback(primeModalReducer, value: \.primeModal, action: \.primeModal),
        pullback(favoritePrimesReducer, value: \.favoritePrimes, action: \.favoritePrimes)
    )
    let appReducer = pullback(_appReducer, value: \.self, action: \.self)

     

    Store

    public final class Store<Value, Action>: ObservableObject {
        private let reducer: (inout Value, Action) -> Void
        // store의 값을 얻는 방법: 프로퍼티를 통해서!
        // value값이 private setter로 되어있기 때문에 모듈 밖에서는 값을 가져오는 것 외에는 아무것도 할 수 없음
        @Published public var value: Value
        private var cancellable: Cancellable?
        
        public init(initialValue: Value, reducer: @escaping (inout Value, Action) -> Void) {
          self.value = initialValue
          self.reducer = reducer
        }
        
        public func send(_ action: Action) {
            self.reducer(&self.value, action)
            print("Action: \(action)")
            print("Value:")
            dump(self.value)
            print("---")
        }
        
        
        // store에 자동으로 변환을 적용하여 appState의 값 중에서 일부만 가져오도록 하는 메서드
        // ((Value) -> LocalValue) -> ((Store<Value, _>) -> Store<LocalValue, _>
        // ((A) -> B) -> ((Store<A, _>) -> Store<B, _>)
        // map: ((A) -> B) -> ((F<A>) -> F<B>)
        
        public func view<LocalValue>(
            _ f: @escaping (Value) -> LocalValue
        ) -> Store<LocalValue, Action> {
            let localStore = Store<LocalValue, Action>(
                initialValue: f(self.value),
                reducer: { localValue, action in
                    self.send(action)
                    localValue = f(self.value)
                }
            )
            localStore.cancellable = self.$value.sink { [weak localStore] newValue in
                localStore?.value = f(newValue)
            }
            return localStore
        }
    }
    
    func transform<A, B, Action>(
      _ reducer: (inout A, Action) -> Void,
      _ f: (A) -> B
    ) -> (inout B, Action) -> Void {
      fatalError()
    }
    
    public func combine<Value, Action>(
        _ reducers: (inout Value, Action) -> Void...
    ) -> (inout Value, Action) -> Void {
        
        return { value, action in
            for reducer in reducers {
                reducer(&value, action)
            }
        }
    }
    
    
    /* =========== < After action pullback > =========== */
    // Global action이 들어올 때 key path를 사용하여 Local Action을 추출하려고 시도
    // -> 성공: reducer로 전달, 실패: 아무것도 안 함
    public func pullback<GlobalValue, LocalValue, GlobalAction, LocalAction>(
        _ reducer: @escaping (inout LocalValue, LocalAction) -> Void,
        value: WritableKeyPath<GlobalValue, LocalValue>,
        action: WritableKeyPath<GlobalAction, LocalAction?>
    ) -> (inout GlobalValue, GlobalAction) -> Void {
        return { globalValue, globalAction in
            guard let localAction = globalAction[keyPath: action] else { return }
            reducer(&globalValue[keyPath: value], localAction)
        }
    }
    
    
    public func logging<Value, Action>(
      _ reducer: @escaping (inout Value, Action) -> Void
    ) -> (inout Value, Action) -> Void {
      return { value, action in
        reducer(&value, action)
        print("Action: \(action)")
        print("State:")
        dump(value)
        print("---")
      }
    }

    'iOS > TCA' 카테고리의 다른 글

    [SwiftUI] 상태관리(Higher-Order Reducers)  (0) 2023.08.02
    [SwiftUI] 상태관리(Action Pullbacks)  (0) 2023.07.18
    [SwiftUI] 상태관리(State Pullbacks)  (0) 2023.07.07
    [SwiftUI] 상태관리(redux)  (0) 2023.07.06
    [SwiftUI] 상태 관리  (0) 2023.07.05
Designed by Tistory.