apple/TCA

[TCA] Effect #5 (Timers)

lgvv 2023. 10. 7. 23:37

[TCA] Effect #5 (Timers)

 

목차

 - Timers 주제에 대해서 간단히 알아보기

 - Timers와 관련한 예제

 

# Timers 주제에 대해서 간단히 알아보기

@Dependency(\.continuousClock) var clock

 

위의 디펜던시 예제처럼 사용할 수 있음 Swift Clocks 라이브러리에서 제공하는 Helper인 클럭에 .timer 메서드를 사용

비동기 코드로 시간을 처리하기 위한 `AsyncSequence` 친화적인 API를 제공

 

#  Timers와 관련한 예제

// MARK: - Feature domain

struct Timers: Reducer {
    struct State: Equatable {
        var isTimerActive = false
        var secondsElapsed = 0
    }
    
    enum Action {
        case onDisappear
        case timerTicked
        case toggleTimerButtonTapped
    }
    
    @Dependency(\.continuousClock) var clock
    private enum CancelID { case timer }
    
    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
        case .onDisappear:
            return .cancel(id: CancelID.timer)
            
        case .timerTicked:
            state.secondsElapsed += 1 // 애니메이션이 toggleTimerButtonTapped에서 적용 받아서 들어옴
            return .none
            
        case .toggleTimerButtonTapped:
            state.isTimerActive.toggle()
            return .run { [isTimerActive = state.isTimerActive] send in
                guard isTimerActive else {
                    // isTimerActive 값이 false 이면 return
                    return
                }
                
                for await _ in self.clock.timer(interval: .seconds(1)) { // ✅ _AsyncTimerSequence<AnyClock<Duration>> 반환되고 라서 for _ 문으로 무한 반복
                    await send(.timerTicked, animation: .interpolatingSpring(stiffness: 3000, damping: 40)) // 애니메이션을 여기서 세팅해서 보내야 한다. 👍 이 애니메이션 좋다~!
                }
            }
            .cancellable(id: CancelID.timer, cancelInFlight: true)
            /*
             cancelInFlight를 사용하는 부분이 특이한데, cancelInFlight는 동일한 효과가 있는지 판별
             
             취소 가능한 effect를 effect로 취소 가능한 를 전환하려면 cancel(id:)에서 취소해야 하는 in-flight 효과를 위해 사용되는 식별자를 제공해야 한다.
             문자열과 같이 해시 가능한 값은 식별자에 사용할 수 있지만, 식별자에 대한 새로운 유형을 정의하여 오타에 대한 약간의 보호 기능을 추가할 수 있습니다.
             
             추측: 아직 완벽하게 이해한건 아니지만 enum 타입으로 식별자를 주는데 이게 String이 아니라서 약간의 보호 기능으로 언급한게 아닐까 싶다.
             */
            
            
        }
    }
}

// MARK: - Feature view

struct TimersView: View {
    let store: StoreOf<Timers>
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            Form {
                AboutView(readMe: readMe)
                
                ZStack {
                    Circle()
                        .fill(
                            AngularGradient(
                                gradient: Gradient(
                                    colors: [
                                        .blue.opacity(0.3),
                                        .blue,
                                        .blue,
                                        .green,
                                        .green,
                                        .yellow,
                                        .yellow,
                                        .red,
                                        .red,
                                        .purple,
                                        .purple,
                                        .purple.opacity(0.3),
                                    ]
                                ),
                                center: .center
                            )
                        )
                        .rotationEffect(.degrees(-90))
                    GeometryReader { proxy in
                        Path { path in
                            path.move(to: CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2))
                            path.addLine(to: CGPoint(x: proxy.size.width / 2, y: 0))
                        }
                        .stroke(.primary, lineWidth: 3)
                        .rotationEffect(.degrees(Double(viewStore.secondsElapsed) * 360 / 60))
                    }
                }
                .aspectRatio(1, contentMode: .fit)
                .frame(maxWidth: 280)
                .frame(maxWidth: .infinity)
                .padding(.vertical, 16)
                
                Button {
                    viewStore.send(.toggleTimerButtonTapped)
                } label: {
                    Text(viewStore.isTimerActive ? "Stop" : "Start")
                        .padding(8)
                }
                .frame(maxWidth: .infinity)
                .tint(viewStore.isTimerActive ? Color.red : .accentColor)
                .buttonStyle(.borderedProminent)
            }
            .navigationTitle("Timers")
            .onDisappear {
                viewStore.send(.onDisappear)
            }
        }
    }
}

// MARK: - SwiftUI previews

struct TimersView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            TimersView(
                store: Store(initialState: Timers.State()) {
                    Timers()
                }
            )
        }
    }
}

 

타이머를 사용할 때는 사이드 이펙트에 주의해야겠다.

 

Timer가 interval로 도는건 for문을 통해 처리하던데 요 부분을 조금 더 면밀하게 살펴보면 좋겠다.

그리고 View쪽도 애니메이션이 정말 잘 구현되어 있는데. 요것도 잘 살펴보자.

 

그리고 무엇보다도 cancelInFlight 부분도 잘 살펴보자!! 

'apple > TCA' 카테고리의 다른 글

[TCA] Navigation (화면전환 총 정리)  (0) 2023.10.09
[TCA] Effect #6 (WebSocket)  (0) 2023.10.08
[TCA] Effect #4 (Refreshable)  (0) 2023.10.07
[TCA] Effect #3 (LongLiving)  (0) 2023.10.07
[TCA] Effect #2 (Cancellation)  (0) 2023.10.07