[SwiftUI][TCA] TCA Case Studies - 03. Timers

별똥별·2025년 2월 27일

TCA

목록 보기
24/24

🚀 TCA에서 Timer 기능 구현하기 – 초 단위 타이머 만들기

안녕하세요, 별똥별🌠입니다!
이번 포스트에서는 TCA를 활용해 Timer 기능을 구현하는 방법을 알아보겠습니다.
이 예제에서는 사용자가 버튼을 눌러 타이머를 시작 및 정지할 수 있으며, 1초마다 카운트가 증가하는 구조를 다룹니다.


🧐 개념 정리

Timer란?

Timer는 일정한 간격으로 동작하는 반복적인 이벤트입니다. iOS에서는 DispatchSourceTimer, Timer, CADisplayLink 등을 사용해 구현할 수 있지만, TCA에서는 Effect와 continuousClock을 활용해 간결하게 처리할 수 있습니다.

Cancellation

타이머가 실행되는 동안 사용자가 화면을 벗어나거나 타이머를 중지해야 할 경우, 불필요한 이벤트가 계속 발생하지 않도록 .cancellable(id:)을 사용해 정리할 수 있습니다.


🎯 코드 분석

1️⃣ Reducer: Timers

@Reducer
struct Timers {
    @ObservableState
    struct State: Equatable {
        var isTimerActive: Bool = false
        var secondsElapsed = 0
    }
    
    enum Action {
        case onDisappear
        case timerTicked
        case toggleTimerButtonTapped
    }
    
    @Dependency(\.continuousClock) var clock
    private enum CancelID { case timer }
    
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .onDisappear:
                return .cancel(id: CancelID.timer)
                
            case .timerTicked:
                state.secondsElapsed += 1
                return .none
                
            case .toggleTimerButtonTapped:
                state.isTimerActive.toggle()
                return .run { [isTimerActive = state.isTimerActive] send in
                    guard isTimerActive else { return }
                    for await _ in self.clock.timer(interval: .seconds(1)) {
                        await send(.timerTicked, animation: .interpolatingSpring(stiffness: 3000, damping: 40))
                    }
                }
                .cancellable(id: CancelID.timer, cancelInFlight: true)
            }
        }
    }
}

주요 포인트

  • 1️⃣ 상태(State)
    • isTimerActive: 타이머가 실행 중인지 여부.
    • secondsElapsed: 초 단위로 증가하는 값.

  • 2️⃣ 액션(Action)
    • .toggleTimerButtonTapped: 타이머 시작/중지 토글.
    • .timerTicked: 1초마다 실행되며 카운트 증가.
    • .onDisappear: 화면이 사라질 때 타이머 정리.

  • 3️⃣ Timer 실행
    • .run 내부에서 clock.timer(interval: .seconds(1))을 사용해 1초마다 .timerTicked을 호출.
    • .cancellable(id:)을 활용해 중복 실행 방지 및 정리.

2️⃣ View: TimersView

struct TimersView: View {
    var store: StoreOf<Timers>
    
    var body: some View {
        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(store.secondsElapsed) * 360 / 60))
                }
            }
            .aspectRatio(contentMode: .fit)
            .frame(maxWidth: .infinity)
            .padding(.vertical, 16)
            
            Button {
                store.send(.toggleTimerButtonTapped)
            } label: {
                Text(store.isTimerActive ? "Stop" : "Start")
                    .padding(8)
            }
            .frame(maxWidth: .infinity)
            .tint(store.isTimerActive ? Color.red : .accentColor)
            .buttonStyle(.borderedProminent)
        }
        .navigationTitle("Timers")
        .onDisappear {
            store.send(.onDisappear)
        }
    }
}

주요 포인트

  • 1️⃣ UI 구성
    • ZStack을 활용해 원형 타이머를 시각화.
    • GeometryReader로 초 바늘 회전 구현.

  • 2️⃣ 타이머 컨트롤
    • Button을 눌러 .toggleTimerButtonTapped을 호출해 실행/중지 가능.
    • onDisappear에서 .onDisappear을 호출하여 타이머 정리.

📊 개념도 요약

1️⃣ 사용자가 버튼을 누르면 .toggleTimerButtonTapped 액션 실행
2️⃣ .run 내부에서 clock.timer(interval: .seconds(1)) 시작
3️⃣ 매 1초마다 .timerTicked 액션 실행 → secondsElapsed 증가
4️⃣ UI가 secondsElapsed 값에 맞춰 원형 타이머를 회전하며 업데이트
5️⃣ 중지 시 .cancellable(id:)으로 타이머 정리


✅ 정리

  • TCA에서 clock.timer(interval:)을 사용해 타이머를 쉽게 구현할 수 있습니다.
  • .cancellable(id:)을 활용하면 중복 실행을 방지하고 안전하게 정리할 수 있습니다.
  • UI와 상태를 연결해 실시간으로 타이머 변화를 반영할 수 있습니다.

이제 TCA로 타이머를 직접 구현해 보세요! ⏳🚀

profile
밍밍

0개의 댓글