TCA에서 AsyncStream 사용해서 타이머 구현하기
타이머를 구현 해야 했습니다
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를 리턴 해주면 됩니다.