[TCA] SharedState
목차
- SharedState란?
- SharedState 예제 코드 알아보기
- SharedState를 통해 통합관리 vs 각각의 Reducer를 통합
- SharedState란?
여러 개의 독립된 화면이 합성 가능한 아키텍처에서 상태를 공유할 수 있는 방법을 제시.
- SharedState 예제 코드 알아보기
// MARK: - Feature domain
struct SharedState: Reducer {
enum Tab { case counter, profile }
struct State: Equatable {
var counter = Counter.State()
var currentTab = Tab.counter
/// The Profile.State can be derived from the Counter.State by getting and setting the parts it
/// cares about. This allows the profile feature to operate on a subset of app state instead of
/// the whole thing.
/// Profile.State는 Counter.State에서 관심있는 부분을 가져와서 설정함으로써 유도할 수 있음. 이를 통해 프로필 기능은 전체가 아닌 앱 상태의 하위 집합에서 작동할 수 있음.
var profile: Profile.State {
get {
// 6. ✅ 하위 뷰에서 get에 정의된 부분을 통해 상위 Reducer의 데이터를 가져갈 수 있음
Profile.State(
currentTab: self.currentTab,
count: self.counter.count,
maxCount: self.counter.maxCount,
minCount: self.counter.minCount,
numberOfCounts: self.counter.numberOfCounts
)
}
set {
// 7. ✅ 하위 뷰에서 발생한 데이터 변경사항을 상위 뷰로도 전달
self.currentTab = newValue.currentTab
self.counter.count = newValue.count
self.counter.maxCount = newValue.maxCount
self.counter.minCount = newValue.minCount
self.counter.numberOfCounts = newValue.numberOfCounts
}
}
}
enum Action: Equatable {
case counter(Counter.Action) // 8. ✅ Counter에서 들어올 액션
case profile(Profile.Action) // 9. ✅ Profile에서 들어올 액션
case selectTab(Tab)
}
var body: some Reducer<State, Action> {
// 9. ✅ counter를 스코프를 통해 연결 (하위뷰를 먼저 연결)
Scope(state: \.counter, action: /Action.counter) {
Counter()
}
// 10. ✅ profile를 스코프를 통해 연결 (하위뷰를 먼저 연결)
Scope(state: \.profile, action: /Action.profile) {
Profile()
}
// 11. ✅ 상위 뷰
Reduce { state, action in
switch action {
case .counter, .profile:
return .none
case let .selectTab(tab):
state.currentTab = tab
return .none
}
}
}
// 12. ✅ Counter에서 사용할 리듀서를 정의
struct Counter: Reducer {
struct State: Equatable {
@PresentationState var alert: AlertState<Action.Alert>?
var count = 0
var maxCount = 0
var minCount = 0
var numberOfCounts = 0
}
enum Action: Equatable {
case alert(PresentationAction<Alert>)
case decrementButtonTapped
case incrementButtonTapped
case isPrimeButtonTapped
enum Alert: Equatable {}
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .alert:
return .none
case .decrementButtonTapped:
state.count -= 1
state.numberOfCounts += 1
state.minCount = min(state.minCount, state.count)
return .none
case .incrementButtonTapped:
state.count += 1
state.numberOfCounts += 1
state.maxCount = max(state.maxCount, state.count)
return .none
case .isPrimeButtonTapped:
state.alert = AlertState {
TextState(
isPrime(state.count)
? "👍 The number \(state.count) is prime!"
: "👎 The number \(state.count) is not prime :("
)
}
return .none
}
}
.ifLet(\.$alert, action: /Action.alert)
}
}
// 12. ✅ Profile에서 사용할 리듀서를 정의
struct Profile: Reducer {
struct State: Equatable {
private(set) var currentTab: Tab
private(set) var count = 0
private(set) var maxCount: Int
private(set) var minCount: Int
private(set) var numberOfCounts: Int
fileprivate mutating func resetCount() {
self.currentTab = .counter
self.count = 0
self.maxCount = 0
self.minCount = 0
self.numberOfCounts = 0
}
}
enum Action: Equatable {
case resetCounterButtonTapped
}
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .resetCounterButtonTapped:
state.resetCount()
return .none
}
}
}
}
// MARK: - Feature view
struct SharedStateView: View {
let store: StoreOf<SharedState>
var body: some View {
// 1. ✅ 해당 뷰에서는 Tab을 observe하고 아래 3번에서 state를 분기하여 주입
WithViewStore(self.store, observe: \.currentTab) { viewStore in
VStack {
Picker(
"Tab",
selection: viewStore.binding(send: SharedState.Action.selectTab) // 2. ✅ selection을 선택한 탭 액션에 바인딩
) {
Text("Counter")
.tag(SharedState.Tab.counter)
Text("Profile")
.tag(SharedState.Tab.profile)
}
.pickerStyle(.segmented)
// 3. ✅ currentTab의 상태에 따라서 state로 분리
if viewStore.state == .counter {
SharedStateCounterView(
store: self.store.scope(state: \.counter, action: SharedState.Action.counter))
}
if viewStore.state == .profile {
SharedStateProfileView(
store: self.store.scope(state: \.profile, action: SharedState.Action.profile))
}
Spacer()
}
}
.padding()
}
}
struct SharedStateCounterView: View {
// 4. ✅ SharedState의 Counter
let store: StoreOf<SharedState.Counter>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack(spacing: 64) {
Text(template: readMe, .caption)
VStack(spacing: 16) {
HStack {
Button {
viewStore.send(.decrementButtonTapped)
} label: {
Image(systemName: "minus")
}
Text("\(viewStore.count)")
.monospacedDigit()
Button {
viewStore.send(.incrementButtonTapped)
} label: {
Image(systemName: "plus")
}
}
Button("Is this prime?") { viewStore.send(.isPrimeButtonTapped) }
}
}
.padding(.top)
.navigationTitle("Shared State Demo")
.alert(store: self.store.scope(state: \.$alert, action: { .alert($0) }))
}
}
}
struct SharedStateProfileView: View {
// 5. ✅ SharedState의 Profile
let store: StoreOf<SharedState.Profile>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack(spacing: 64) {
Text(
template: """
This tab shows state from the previous tab, and it is capable of reseting all of the \
state back to 0.
This shows that it is possible for each screen to model its state in the way that makes \
the most sense for it, while still allowing the state and mutations to be shared \
across independent screens.
""",
.caption
)
VStack(spacing: 16) {
Text("Current count: \(viewStore.count)")
Text("Max count: \(viewStore.maxCount)")
Text("Min count: \(viewStore.minCount)")
Text("Total number of count events: \(viewStore.numberOfCounts)")
Button("Reset") { viewStore.send(.resetCounterButtonTapped) }
}
}
.padding(.top)
.navigationTitle("Profile")
}
}
}
// MARK: - SwiftUI previews
struct SharedState_Previews: PreviewProvider {
static var previews: some View {
SharedStateView(
store: Store(initialState: SharedState.State()) {
SharedState()
}
)
}
}
// MARK: - Private helpers
/// Checks if a number is prime or not.
private func isPrime(_ p: Int) -> Bool {
if p <= 1 { return false }
if p <= 3 { return true }
for i in 2...Int(sqrtf(Float(p))) {
if p % i == 0 { return false }
}
return true
}
SharedStateView는 CurrentTab에 대한 정보를 관리하고, ViewWithStore에서 SharedReducer의 tab 정보만 가지고 있는다.
SharedStateView 하위에 ProfileView와 CounterView를 두고 있다.
- SharedState를 통해 통합관리 vs 각각의 Reducer를 통합
위의 예제는 Reducer를 채택한 SharedState 통해서 통합적으로 관리된다.
그러나 CounterView와 ProfileView가 각각 독립적으로 Reducer를 가지는 방법도 존재한다.
이 경우에는 Delegate를 통해서 위와 동일한 기능으로 구현할 수 있다.
Delegate를 사용하는 방식은 추후에 Effect를 학습하면서 기술할 예정이다.
이 부분이 궁금하다면 TCA Tutorial의 ContactFeature를 참고하기 바란다.
(참고)
'apple > TCA' 카테고리의 다른 글
[TCA] Effect #2 (Cancellation) (0) | 2023.10.07 |
---|---|
[TCA] Effect #1 (Basics) (1) | 2023.10.07 |
[TCA] OptionalState (IfLetCase) (0) | 2023.09.27 |
[TCA] FocusState (0) | 2023.09.27 |
[TCA] Binding (0) | 2023.09.27 |