ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TCA - IfLetStore
    TCA 2022. 10. 11. 15:14

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 TCA에서의 IfLetStore라는 구조체에 대해 알아보겠습니다🙌

     

    우선 오늘 포스팅될 내용은 전부 ComposableArchitecture에서 IfLetStore 정의 및 구현된 부분의 코드를 뜯어봤습니다.

    해당 전체 코드가 궁금하신 분들은 아래 링크를 참고해주세요!

    https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift

     

    GitHub - pointfreeco/swift-composable-architecture: A library for building applications in a consistent and understandable way,

    A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. - GitHub - pointfreeco/swift-composable-architecture: A library for bu...

    github.com

     

    그럼 본격적으로 코드를  뜯어보죠🦷

     

    IfLetStore?

    해당 구조체는 SwiftUI를 임포트하여 사용하고 있기에 주로 SwiftUI 환경에서 사용됩니다.

    그럼 이 구조체 타입은 어떨때 쓰는지 정의를 해보겠습니다.

    두 뷰 중 하나를 나타내기 위해서 옵셔널한 상태의 Store를 안전하게 언래핑 하는 뷰입니다.

    기본 State, 즉 상태가 nil이 아닐때 then 클로저가 Store를 수행하게 됩니다.

    만약 옵셔널 상태가 아닐 경우(해당되지 않을때, 상태가 nil일때) else 클로저를 수행하게 됩니다.

    즉 정리해보면 이 기능 자체는 상태, State에 따라 두 뷰중 하나를 나타내기 위해 결정되는데 쓰입니다.

     

    음... 조금 말이 어려울 수 있어요.

    그런데 정의가 그런거지 실제로 구현을 보고 느낀다면 쉬울거에요.

    같이 해보시죠!

     

    IfLetStore 구현 정의

    그럼 IfLetStore의 정의가 어떻게 구현되어 있는지 아래 코드를 보시죠.

    public struct IfLetStore<State, Action, Content: View>: View {
      private let content: (ViewStore<State?, Action>) -> Content
      private let store: Store<State?, Action>
      
      public init<IfContent, ElseContent>(
        _ store: Store<State?, Action>,
        @ViewBuilder then ifContent: @escaping (Store<State, Action>) -> IfContent,
        @ViewBuilder else elseContent: @escaping () -> ElseContent
      ) where Content == _ConditionalContent<IfContent, ElseContent> {
        self.store = store
        self.content = { viewStore in
          if var state = viewStore.state {
            return ViewBuilder.buildEither(
              first: ifContent(
                store.scope {
                  state = $0 ?? state
                  return state
                }
              )
            )
          } else {
            return ViewBuilder.buildEither(second: elseContent())
          }
        }
      }
      
      public init<IfContent>(
        _ store: Store<State?, Action>,
        @ViewBuilder then ifContent: @escaping (Store<State, Action>) -> IfContent
      ) where Content == IfContent? {
        self.store = store
        self.content = { viewStore in
          if var state = viewStore.state {
            return ifContent(
              store.scope {
                state = $0 ?? state
                return state
              }
            )
          } else {
            return nil
          }
        }
      }
      
      public var body: some View {
        WithViewStore(
          self.store,
          removeDuplicates: { ($0 != nil) == ($1 != nil) },
          content: self.content
        )
      }
    }

    이렇게 간단하게? 구현되어 있습니다.

    우선 State와 Action, Content를 제네릭하게 받으며 View 프로토콜을 따릅니다.

    View라는 소리일것이고 Content도 View 프로토콜을 따르니 마찬가지로 View를 나타냅니다.

    사용되는 프로퍼티를 보면 ViewStore를 받아 Content(View)를 뱉어주는 content와 Store 타입의 store가 있습니다.

    아래 두개의 이니셜라이저를 보겠습니다.

     

    첫번째 이니셜라이저로는 then 파라미터와 else 파라미터가 있습니다.

    모두 ViewBuilder 프로퍼터래퍼를 사용한 타입이죠?

    then으로 넣어준 뷰는 조건을 판별할 state의 값이 nil이 아닐때 보여줄 뷰를 나타내고,

    else로 넣어준 뷰는 state 값이 nil일때 보여줄 뷰를 나타내줍니다.

    그래서 이 두 파라미터를 가지고 내부 프로퍼티인 content를 만들어 주게 됩니다.

    이에 대한 판별 또한 where 조건을 통해 _ConditionalContent 타입에서 IfContent와 ElseContent를 제네릭으로 받아 판별해주죠.

     

    두번째 이니셜라이저는 보시면 else 파라미터가 없죠?

    그말인 즉슨 else일 경우 별도 뷰를 주지 않아도 에러가 나지 않으며 단순히 뷰의 노출이 없다는 차이가 있습니다.

     

    즉 then 파라미터든 else 파라미터든 원하는 방식으로 조합해 사용할 수 있습니다.

    그렇게 이닛 시 정해진 content를 가지고 실제로 View의 body를 만들어주게 됩니다.

     

    그럼 이제 어떻게 여러 방면으로 사용할 수 있는지 보겠습니다!

     

    IfLetStore 사용법

    // else 사용
    IfLetStore(
      store.scope(state: \SearchState.results, action: SearchAction.results),
    ) {
      SearchResultsView(store: $0)
    } else: {
      Text("Loading search results...")
    }
    
    // then 사용
    IfLetStore(
      store.scope(state: \SearchState.results, action: SearchAction.results),
      then: { store in 
        SearchResultsView(store: store)
      }
    )
    
    // then, else 사용
    IfLetStore(
      store.scope(state: \SearchState.results, action: SearchAction.results),
      then: { store in 
        SearchResultsView(store: store)
      },
      else: {
        Text("Loading search results...")
      }
    )

    기본 여러 사용은 이렇습니다.

    아까 말했듯 SearchState가 아마 하위 State일것이고 상위 State에서 이 State의 results값을 옵셔널로 가지고 있을것 같아요.

    즉 해당 results값의 초기값은 nil일테고 무언가 값이 할당이 되면 SearchResultView를 보여주게 됩니다.

    여기서 else 구문을 보시면 results값이 nil이여서 값의 할당이 이뤄지기 전이라면 Text를 보여주게 됩니다.

    만약 해당 값이 없을때 아무 뷰도 나타내고 싶지 않다면 이 else 구문은 없어도 됩니다.

    아래처럼 사용될 수 있습니다.

    .sheet(
      isActive: viewStore.binding(
        get: \.isGameActive,
        send: { $0 ? .startButtonTapped : .detailDismissed }
      )
    ) {
      IfLetStore(
        self.store.scope(state: \.detail, action: AppAction.detail)
      ) {
        DetailView(store: $0)
      }
    }

    보시면 시트가 나타나면 해당 시트에 해당하는 뷰는 DetailView일것이고 이 뷰가 나타나는 조건은 detail State가 nil이 아니여야 합니다.

    만약 시트가 나타날때 detail값이 nil이면 내부에 아무 뷰도 뜨지 않은 빈 시트가 나올겁니다.

    여기서의 sheetView가 뜨는것과 sheetView 내부에 보여줄 뷰는 별개로 동작한다 보시면 됩니다😄

     

    마무리

    이렇게 간단한 주제이기도 해서 그에 맞게 간단히 IfLetStore를 알아봤습니다!

    저는 자주 쓰고 있기에 잘 활용하면 좋을 것 같습니다🙌

    'TCA' 카테고리의 다른 글

    TCA - Debouncing  (0) 2022.10.17
    TCA - Timer  (2) 2022.10.13
    TCA - Debugging  (2) 2022.10.06
    TCA - Scope  (7) 2022.09.26
    TCA - pullback  (0) 2022.09.19
Designed by Tistory.