apple/TCA

[TCA] Effect #1 (Basics)

lgvv 2023. 10. 7. 15:57

[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