개발/Swift

TCA에서 AsyncStream 사용해서 타이머 구현하기

덤벨로퍼 2025. 1. 15. 23:18

타이머를 구현 해야 했습니다

TCA + SwiftUI 기반으로 구현중이라 여기에 걸맞게 구현해야 합니다

그래서 Reducer에서 타이머를 구현하고 카운트다운 State를 설정하여

View에서 바인딩하는 방식으로 구현하려 합니다

 

우선 AsyncStream 사용해본적이 없어서 이거부터 만들어봤습니다

간단히 60 부터 0까지 반복문 + Task.sleep을 넣어 1초마다 호출되도록 하였습니다

Task {
   for i in (0...59).reversed() {
       try await Task.sleep(nanoseconds: 1_000_000_000)
       print(i)
   }
}

// 59..58..57..56

이제 저 i를 AsyncStream 에 담아서 계속 전송해주도록 할겁니다

private func timerStream() -> AsyncStream<Int> {
         AsyncStream<Int> { continuation in
             Task {
                 for i in (0...59).reversed() {
                     try await Task.sleep(nanoseconds: 1_000_000_000)
                     continuation.yield(i)
                 }
                 continuation.finish()
             }
        }
    }

이렇게 되면 yield 를 통해 계속 배출(?)이 되고 60초가 끝나면 finish를 통해 끝이 납니다

yield에 넣어준 i 가 Int 이므로 AsyncStream<Int> 타입이 됩니다

이제 AsyncStream<Int> 을 사용해야하는데 for await을 통해 가능합니다

 for await time in timerStream() {
      print(time)
  }

이렇게 1초마다 time이 방출이 되었고 이것을 TCA에 연결하여

초마다 상태값을 바꿔야하며

외부에서도 끌수 있어합니다 (ex> 페이지 닫을때 타이머 종료)

private enum CancelID { case timer }

private func startTimer() -> Effect<Action> {
        return .run { send in
            for await time in timerStream() {
                await send(.timer(time))
            }
        }.cancellable(id: CancelID.timer, cancelInFlight: true)
    }

startTimer 함수는 Effect<Action> 을 방출합니다 Reducer 내부 코드이므로 Reducer의 Action d입니다

send(Action)을 통해 시간을 전달 해주었습니다 이는 reduce 내부에서 상태를 바꿔줄겁니다

또 cancellable을 지정해줬습니다 id 값이 필요한데 enum 으로 넣어줬습니다 (TCA 공식 예제 참고 함)

//Reducer 

  case .requestDone: //타이머 시작 액션
      return startTimer()
  case let .timer(time):
      state.time = time

리듀스 부분에서 startTimer 를 넣어줬고 타이머가 동작하게 됩니다

그러면 초마다 .timer 액션이 돌게되고 상태값을 변경하여 View에서 저 상태를 보고 Ui 처리해주면 됩니다

만약 페이지가 닫혀서 타이머를 끈다면

  case .close:
      return .cancel(id: CancelID.timer)

이런식으로 cancel Effect를 리턴 해주면 됩니다.