본문 바로가기
Swift/TCA

[TCA] day 13 타이머 (Effects- Timer)

by 마라민초닭발로제 2024. 5. 8.

코드 전문

import ComposableArchitecture
import SwiftUI

private let readMe = """
  This application demonstrates how to work with timers in the Composable Architecture.

  It makes use of the `.timer` method on clocks, which is a helper provided by the Swift Clocks \
  library included with this library. The helper provides an `AsyncSequence`-friendly API for \
  dealing with times in asynchronous code.
  """

@Reducer
struct Timers {
  @ObservableState
  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 }

  var body: some Reducer<State, Action> {
    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)
      }
    }
  }
}

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(1, contentMode: .fit)
      .frame(maxWidth: 280)
      .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)
    }
  }
}

#Preview {
  NavigationStack {
    TimersView(
      store: Store(initialState: Timers.State()) {
        Timers()
      }
    )
  }
}

 

 

동작화면

 

 

리드미 해석

This application demonstrates how to work with timers in the Composable Architecture. It utilizes the .timer method on clocks, which is a helper provided by the Swift Clocks library included with this library. The helper provides an AsyncSequence-friendly API for dealing with times in asynchronous code.

 

이 애플리케이션은 Composable Architecture에서 타이머를 다루는 방법을 보여줍니다. Swift Clocks 라이브러리에 포함된 .timer 메서드를 활용합니다. 이 메서드는 비동기 코드에서 시간을 다루는데 도움을 주는 헬퍼입니다. 이 헬퍼는 AsyncSequence와 호환되는 API를 제공합니다.

 

 

 

타이머 동작 과정

타이머 버튼을 누를 경우, isTimerActive변수를 통해서 타이머가 움직일지 움직이지 않을지를 결정합니다. 그리고 cancellable에 등록을 합니다. cancellable의 cancelInFlight을 통해서 기존에 작업하던 동작을 취소해 줍니다.

우리는 task를 cancelid 에 할당하고 타이머를 동작시킵니다. 그리고 버튼이 한번더 눌렸을 경우에 이전에 사용하고 있는 cancelid를 통해서 작업을 재 할당 시켜야 합니다. 이 때 과거에 추가된 Task를 취소시켜야 합니다. 이를 cancellable의 cancelFlight를 통해 제거합니다.

 

cancelFlight: 새로운 것을 시작하기전에, 같은 identifier의 내부 effect 를 취소시킬지 말지를 결정합니다. 

cancellable docs

 

 

 

 

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)

 

게시물은 TCA 라이브러리의 CaseStudies를 직접 작성해보고 어떻게 작동하는지에 대해서 공부하기 위해서 작성했습니다.