[TCA] Effect #1 (Basics)
목차
- Effect란?
- Effect Basic 예제 살펴보기
- Effect란?
TCA로 만들어진 기능에 Side Effect을 도입할 수 있음.
Side Effect란 외부에서 수행되어야 하는 작업들로 API 요청, HTTP를 통해 외부 서비스를 사용하는 등 불확실하며 복잡하기도 함.
- 영어 용어 정리
NB: nota bene라는 라틴어로 주의, 유의라는 의미
- Effect Basic 예제 살펴보기
import ComposableArchitecture
import SwiftUI
// MARK: - Feature domain
struct EffectsBasics: Reducer {
struct State: Equatable {
var count = 0
var isNumberFactRequestInFlight = false
var numberFact: String?
}
enum Action: Equatable {
case decrementButtonTapped
case decrementDelayResponse
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(TaskResult<String>)
}
@Dependency(\.continuousClock) var clock
@Dependency(\.factClient) var factClient
private enum CancelID { case delay }
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .decrementButtonTapped:
state.count -= 1
state.numberFact = nil
// 만약 수가 음수라면 1초 후에 수를 다시 1 재증가 시켜주는 구현
return state.count >= 0
? .none
: .run { send in
try await self.clock.sleep(for: .seconds(1)) // 1초 기다렸다가
await send(.decrementDelayResponse) // 1초 뒤 딜레이
}
.cancellable(id: CancelID.delay) // 추측?: 해당 이벤트를 구독하고, 특정 시점에 취소 시키기 위해 이벤트를 취소시키기 위하여 등록
case .decrementDelayResponse:
if state.count < 0 {
state.count += 1 // 감소 이벤트가 도착하면 양수일 때만 올림
}
return .none
case .incrementButtonTapped:
state.count += 1
state.numberFact = nil
return state.count >= 0
? .cancel(id: CancelID.delay) // cancel을 통한 딜레이 바인딩 제거
: .none
case .numberFactButtonTapped:
state.isNumberFactRequestInFlight = true
state.numberFact = nil
// API를 통해 받아 온 값을 반환한다
// `numberFactResponse` action으로 value 반환
return .run { [count = state.count] send in
// TaskResult는 enum으로 Result와 동일하게 사용 가능
await send(.numberFactResponse(TaskResult { try await self.factClient.fetch(count) }))
}
case let .numberFactResponse(.success(response)):
state.isNumberFactRequestInFlight = false
state.numberFact = response
return .none
case .numberFactResponse(.failure):
// 주의: 경고 알럿 같은 방법으로 에러를 핸들링을 할 수 있는 부분
state.isNumberFactRequestInFlight = false
return .none
}
}
}
// MARK: - Feature view
struct EffectsBasicsView: View {
let store: StoreOf<EffectsBasics>
@Environment(\.openURL) var openURL // 외부에서 정의되어 있음
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
Form {
Section {
AboutView(readMe: readMe)
}
Section {
HStack {
Button {
viewStore.send(.decrementButtonTapped)
} label: {
Image(systemName: "minus")
}
Text("\(viewStore.count)")
.monospacedDigit()
Button {
viewStore.send(.incrementButtonTapped)
} label: {
Image(systemName: "plus")
}
}
.frame(maxWidth: .infinity)
Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
.frame(maxWidth: .infinity)
if viewStore.isNumberFactRequestInFlight {
ProgressView()
.frame(maxWidth: .infinity)
// Swift UI에 새로운 ID가 주어지지 않으면 프로그레스 뷰가 두번째에는 나타나지 않는 버그가 있는 것 같다고 함.
// 한번은 나타나고 두번은 나타나지 않는다는 말
.id(UUID())
}
if let numberFact = viewStore.numberFact {
Text(numberFact)
}
}
Section {
Button("Number facts provided by numbersapi.com") {
self.openURL(URL(string: "http://numbersapi.com")!)
}
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderless)
}
.navigationTitle("Effects")
}
}
// MARK: - SwiftUI previews
struct EffectsBasicsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
EffectsBasicsView(
store: Store(initialState: EffectsBasics.State()) {
EffectsBasics()
}
)
}
}
}
TaskResult와 enum Cancel { } 을 사용하는 부분을 알게 되었음.
특히 enum Cancel 부분은 조금 더 세밀하게 컨트롤 하는게 필요할 듯 싶다
'apple > TCA' 카테고리의 다른 글
[TCA] Effect #3 (LongLiving) (0) | 2023.10.07 |
---|---|
[TCA] Effect #2 (Cancellation) (0) | 2023.10.07 |
[TCA] SharedState (1) | 2023.09.27 |
[TCA] OptionalState (IfLetCase) (0) | 2023.09.27 |
[TCA] FocusState (0) | 2023.09.27 |