기능의 상태, 작업, 논리 및 동작을 캡슐화
struct CounterFeature: Reducer {
struct State {
var count = 0
var fact: String?
var isTimerOn = false
}
}
enum Action {
// 이름 작성 시 UI를 명확하게 알 수 있도록
case decrementButtonTapped
case getFactButtonTapped
case incrementButtonTapped
case toggleTimerButtonTapped
}
state
, action
을 받고, Effect
를 반환var body: some ReducerOf<Self> {
// Reduce(reduce: (inout State, Action) -> Effect<Action>)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .decrementButtonTapped:
code
case .getFactButtonTapped:
code
case .incrementButtonTapped:
code
case .toggleTimerButtonTapped:
code
}
}
}
✅ 각 UI 기능 별 로직을 구현
이때 state는 inout이기 때문에 직접 변경할 수 있음
✅ Effect 형식의 인스턴스를 반환
→ Effect 인스턴스는 real world에서 실행되고, 데이터를 시스템에 다시 공급함
→ ex. API Request, Apple 프레임워크와 상호작용 등
.none
⭐️ .run
case .getFactButtonTapped:
return .run(
operation: (Send<Action>) async throws -> Void
)
send
를 전달case .getFactButtonTapped:
return .run { send in
try await URLSession.shared.data(
from: URL(
string: "<http://www.numbersapi.com/\\(state.count)>"
)!
)
}
🚨⚒️ 위 예제의 state는 inout이기 때문에 값 캡쳐가 필요함
case .getFactButtonTapped:
return .run { [count = state.count] send in
try await URLSession.shared.data(
from: URL(
string: "<http://www.numbersapi.com/\\(count)>"
)!
)
}
🚨 해당 값을 Effect에서 inout state 값에 바로 반영할 수 없음
→ Effect에서 state를 변경하는 대신 새로운 action을 시스템으로 보냄
→ Reducer가 action에 반응하여 state를 변경하거나 Effect를 실행할 수 있음
⚒️ respond를 다시 시스템으로 보낼 새로운 action을 추가하고 Effect의 후행 클로저인 send를 사용하여 새 작업을 내보냄
enum Action {
case factResponse(String)
...
case let .factResponse(fact):
state.fact = fact
return .send(.nextAction)
return .none
...
case .getFactButtonTapped:
return .run { [count = state.count] send in
let (data, _) = try await URLSession.shared.data(
from: URL(
string: "<http://www.numbersapi.com/\\(count)>"
)!
)
let fact = String(decoding: data, as: UTF8.self)
await send(.factResponse(fact))
}
⭐️ .cancellable
Hashable한 id를 제공하여 작업 취소
return .run { send in
…
}
.cancellable(id: Hashable)
Reducer내부에 NameSpace를 생성하여 오타를 줄이는 것도 좋다
private enum CancelID { case timer }
.cancellable(id: CancelID.timer)
struct ContentView: View {
let store: Store<CounterFeature.State, CounterFeature.Action>
// 축약형
let store: StoreOf<CounterFeature>
…
}
reducer
, state
, action
모두 값타입이기 때문에, 예측 불가능한 외부 시스템과 상호 작용하기 위해서 참조타입의 Store가 필요var body: some View {
WithViewStore(
self.store,
observe: (CounterFeature.State) -> ViewState
) { viewStore in
Form {
…
}
}
}
observe
클로저에 실제 관찰하려는 저장소를 전달하여 실제로 관찰하려는 state 부분을 결정할 수 있음
→ 보통 필요 이상으로 state를 갖고 있기 때문에 필요한 기능만을 선택하여 사용하도록 해라
🔥 17부터는 observable 사용하여 ViewStore없이 타게팅을 단순화할 수 있을 것
View(ContentView)의 store에 Reducer를 채택하여 만든 CounterFeature의 인스턴스를 넣어줌
→ Store에는 기능을 시작할 초기 상태를, 후행 클로저에는 설명되는 기능의 논리를 구동하는 Reducer를 넣음
import ComposableArchitecture
import SwiftUI
@main
struct CounterApp: App {
var body: some Scene {
WindowGroup {
ContentView(
store: Store(
initialState: CounterFeature.State()
) {
CounterFeature()
}
)
}
}
}
기능의 모든 논리가 값타입으로 모델링