코드 전문
import ComposableArchitecture
import SwiftUI
private let readMe = """
This screen demonstrates how one can cancel in-flight effects in the Composable Architecture.
Use the stepper to count to a number, and then tap the "Number fact" button to fetch \
a random fact about that number using an API.
While the API request is in-flight, you can tap "Cancel" to cancel the effect and prevent \
it from feeding data back into the application. Interacting with the stepper while a \
request is in-flight will also cancel it.
"""
@Reducer
struct EffectsCancellation {
@ObservableState
struct State: Equatable {
var count = 0
var currentFact: String?
var isFactRequestInFlight = false
}
enum Action {
case cancelButtonTapped
case stepperChanged(Int)
case factButtonTapped
case factResponse(Result<String, Error>)
}
@Dependency(\.factClient) var factClient
private enum CancelID { case factRequest }
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .cancelButtonTapped:
state.isFactRequestInFlight = false
return .cancel(id: CancelID.factRequest)
case let .stepperChanged(value):
state.count = value
state.currentFact = nil
state.isFactRequestInFlight = false
return .cancel(id: CancelID.factRequest)
case .factButtonTapped:
state.currentFact = nil
state.isFactRequestInFlight = true
return .run { [count = state.count] send in
await send(.factResponse(Result { try await self.factClient.fetch(count) }))
}
.cancellable(id: CancelID.factRequest)
case let .factResponse(.success(response)):
state.isFactRequestInFlight = false
state.currentFact = response
return .none
case .factResponse(.failure):
state.isFactRequestInFlight = false
return .none
}
}
}
}
struct EffectsCancellationView: View {
@Bindable var store: StoreOf<EffectsCancellation>
@Environment(\.openURL) var openURL
var body: some View {
Form {
Section {
AboutView(readMe: readMe)
}
Section {
Stepper("\(store.count)", value: $store.count.sending(\.stepperChanged))
if store.isFactRequestInFlight {
HStack {
Button("Cancel") { store.send(.cancelButtonTapped) }
Spacer()
ProgressView()
// NB: There seems to be a bug in SwiftUI where the progress view does not show
// a second time unless it is given a new identity.
.id(UUID())
}
} else {
Button("Number fact") { store.send(.factButtonTapped) }
.disabled(store.isFactRequestInFlight)
}
if let fact = store.currentFact {
Text(fact).padding(.vertical, 8)
}
}
Section {
Button("Number facts provided by numbersapi.com") {
self.openURL(URL(string: "http://numbersapi.com")!)
}
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderless)
.navigationTitle("Effect cancellation")
}
}
#Preview {
NavigationStack {
EffectsCancellationView(
store: Store(initialState: EffectsCancellation.State()) {
EffectsCancellation()
}
)
}
}
동작 화면
리드미 해석
This screen demonstrates how one can cancel in-flight effects in the Composable Architecture.
Use the stepper to count to a number, and then tap the "Number fact" button to fetch a random fact about that number using an API.
While the API request is in-flight, you can tap "Cancel" to cancel the effect and prevent it from feeding data back into the application. Interacting with the stepper while a request is in-flight will also cancel it.
이 화면은 컴포저블 아키텍처에서 기내 효과를 취소하는 방법을 보여줍니다.
스텝퍼를 사용하여 숫자를 세고 '숫자 사실' 버튼을 탭하여 API를 사용하여 해당 숫자에 대한 임의의 사실을 가져옵니다.
API 요청이 진행 중일 때 '취소'를 탭하여 효과를 취소하고 애플리케이션에 데이터를 다시 공급하지 못하게 할 수 있습니다. 요청이 실행 중일 때 스텝퍼와 상호 작용하면 요청도 취소됩니다.
enum CancelID { case feactRequest}
우리는 API요청이후에 request Fecth를 능동적으로 취소할 수 있어야 합니다. 이를 위해서 tca에서는 sideEffect에서 값을 불러오는 중간에 cancel을할 수 있는 기능을 만들어 놨습니다. 흔하게 combine cancelable에서 활용하듯이 cancel하면 됩니다. 사용법은 다음과 같습니다.
factButton이 눌렀을 경우
외부 effect를 실행하게 만듭니다. 이 때 fecthResponse에 Result를 send하게 됩니다. 우리는 이 effect요청 작업의 단위를 저장할 수 있는데 이것이 cancellable(id: CancelID.factRequest)입니다.
만약 특정한 작업으로 인해 API요청을 취소시킨다면, cancellable을 통해서 취소시키면 됩니다.
case .factButtonTapped:
state.currentFact = nil
state.isFactRequestInFlight = true
return .run { [count = state.count] send in
await send(.factResponse(Result { try await self.factClient.fetch(count) }))
}
.cancellable(id: CancelID.factRequest)
이렇게 return에 .cancel을 통해서 effect를 취소시키면 됩니다.
case .cancelButtonTapped:
state.isFactRequestInFlight = false
return .cancel(id: CancelID.factRequest)
Reducer run effect는 동기 비동기?
현재 공식문서 예제는 다음과 같이 코드가 작성되어 있습니다. 그런데 보시는 것 과 같이 cancel과 관련된 비슷한 로직이 있습니다. 비슷한 로직이 있을 때 .run effect를 실행하여 다른 effect에 send하는 방법으로 구성해 보면 일관된 코드작성을 할 수 있을 것입니다.
case .cancelButtonTapped:
state.isFactRequestInFlight = false
return .cancel(id: CancelID.factRequest)
// Before
case let .stepperChanged(value):
state.count = value
state.currentFact = nil
state.isFactRequestInFlight = false
return .cancel(id: CancelID.factRequest)
// After
case let .stepperChanged(value):
state.count = value
state.currentFact = nil
return .run { send in
await send(.cancelButtonTapped)
}
그런데 이런 의문이 들 수도 있습니다. API통신도 run { await }을 활용하고 cancell도 run {await} 인데, 동기적으로 실행된다면, cancel은 API통신 이후에 실행되는거 아닌가? 그래서 확인해 보니 비동기 적으로 실행되는 것을 확인했습니다.
정상 작동시 effects가 들어옵니다. 하지만 stteper값을 변경하면 factresponse api통신이 취소되는것을 확인하였습니다.
게시물은 TCA 라이브러리의 CaseStudies를 직접 작성해보고 어떻게 작동하는지에 대해서 공부하기 위해서 작성했습니다.
'Swift > TCA' 카테고리의 다른 글
[TCA] day 12 새로고침 (Effects-Refreshable) (0) | 2024.05.07 |
---|---|
[TCA] day 10 long living Effect (publisher Effect and async/await effect) (0) | 2024.05.05 |
[TCA] TCA 1.10 이후부터의 Shared State의 initRule (0) | 2024.05.02 |
[TCA] day 9 Shared State활용한 TodosList 예제 (TCA @Shared, Sheet) (0) | 2024.05.01 |
[TCA] day 8 TCA 1.10 이후부터의 Shared State 관리(@Shared) (0) | 2024.04.30 |