최근 TCA와 SwiftUI를 시작하면서 공부할게 너무 많아졌다.. 한번 끄적거려 보자..
TCA를 사용하면 항상 View에 Store, ViewStore를 사용하는데, 이 차이는 뭘까.
Store의 구현부를 보면
public final class Store<State, Action>
로 선언되어 있고,
ViewStore의 구현부는
public final class ViewStore<ViewState, ViewAction>: ObservableObject
로 선언되어 있다.
일단 이름만으로 예측해보면, View가 prefix로 붙어있고, Combine의 ObservableOject를 채택하고 있다. 따라서 “Store를 관찰하면서 View를 변경하겠다” 라는 생각이 든다.
Store, ViewStore를 사용하는 부분을 보면,
1.
struct FeatureView: View {
let store: StoreOf<Feature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
// A large, complex view inside here...
}
}
}
2.
struct FeatureView: View {
let store: StoreOf<Feature>
@ObservedObject var viewStore: ViewStoreOf<Feature>
init(store: StoreOf<Feature>) {
self.store = store
self.viewStore = ViewStore(self.store, observe: { $0 })
}
var body: some View {
// A large, complex view inside here...
}
}
이런 식으로 store 프로퍼티를 선언하고,
WithViewStore를 사용해 모델을 관찰하거나, viewStore 프로퍼티를 만들어 모델을 관찰한다. 왜 관찰한다고 표현하냐면 viewStore의 initializer 부분을 보면 알 수 있다. viewStore(self.store, observe: { $0 }) 형식으로 ViewStore가 선언되는데,
init 함수로 가보면,
public init<State>(
_ store: Store<State, ViewAction>,
observe toViewState: @escaping (State) -> ViewState,
removeDuplicates isDuplicate: @escaping (ViewState, ViewState) -> Bool
) {
self._send = { store.send($0) }
self._state = CurrentValueRelay(toViewState(store.state.value))
self.viewCancellable = store.state
.map(toViewState)
.removeDuplicates(by: isDuplicate)
.sink { [weak objectWillChange = self.objectWillChange, weak _state = self._state] in
guard let objectWillChange = objectWillChange, let _state = _state else { return }
objectWillChange.send()
_state.value = $0
}
}
선언부의 로직에 가보면,
self._state = CurrentValueRelay(toViewState(store.state.value))를 볼 수 있다. 코드를 살펴보면, self._state에 CurrentValueRelay 라는 실패없는 Publisher 객체에 값을 만든다. 프로퍼티를 선언할때 observe 클로져가 실행되는데, 현재 store.state.value들을 가지고, ViewState를 반환한다.
store.state는 CurrentValueSubject를 말하고, CurrentValueSubject는 가장 최신의 State을 가지고 있다(Store의 initializer에서 초기 State를 받는 이유). State를 ViewState로 변환해, 변화를 관찰할 상태로 변환한다. 즉, ViewStore는 Store의 State가 변화할때마다 관찰하고 Action을 보낼 수도 있다고 예측할 수 있다. 따라서 ViewStore는 State가 변화할 때마다 Store의 Scope를 잡아 지속적으로 이벤트를 관찰하므로 View의 상태를 바꿀 수 있게 되는 것.
그럼 ViewStore를 선언할 때만 Store를 만들고 ViewStore만 프로퍼티로 가지고 있으면 되는거 아니냐 할 수 있는데, ViewStore는 Store의 객체를 가지고 있지 않고, Store의 “상태”만을 관찰한다. ViewStore, Store 둘 다 class 이기 때문에 당연하게도 Store 객체를 가지고 있지 않으면, 메모리에서 해제됨으로 Reducer의 Action 또한 못 받기에 객체를 가지고 있어야한다.
번외) StoreOf, ViewStoreOf
StoreOf, ViewStoreOf는 선언을 좀더 쉽게 만들기 위한 Typealias이다.
public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
public typealias ViewStoreOf<R: ReducerProtocol> = ViewStore<R.State, R.Action>
그리고, WithViewStore를 사용할 경우 content로 viewStore를 뱉어주는데, 이게 viewStore가 무엇인지 값을 명시해주지 않으면 컴파일러 추론을 한다. 따라서 컴파일 시간이 길어지게 되고, 심하게는 Type-check 시간이 너무 길어 컴파일이 되지 않는 오류를 겪게 된다(실제로 겪어봄..). 이를 해결하기 위해
WithViewStore(self.store, observe: { $0 }) { (viewStore: ViewStoreOf<Feature>) in
// A large, complex view inside here...
}
직접 WithViewStore에 ViewStore가 무엇인지 명시해주거나,
struct FeatureView: View {
let store: StoreOf<Feature>
@ObservedObject var viewStore: ViewStoreOf<Feature>
init(store: StoreOf<Feature>) {
self.store = store
self.viewStore = ViewStore(self.store, observe: { $0 })
}
var body: some View {
// A large, complex view inside here...
}
}
ViewStore를 프로퍼티로 선언해 추론을 못하게 막는 방법이 있다.
참고
'iOS > 학습' 카테고리의 다른 글
Error) failed to demangle superclass of ‘…’ from mangled name ‘…’ (3) | 2023.05.03 |
---|---|
SwiftUI) iOS13 부터 지원하는 간단한 커스텀 Attribute Text 만들기 (0) | 2023.02.15 |
Storyboard 에서 Generic Type 의존성 주입하기 2 (0) | 2022.08.29 |
Custom Horizontal FlowLayout (0) | 2022.06.21 |
RxNimble + Quick Unit Test (0) | 2022.05.26 |