안녕하세요, 별똥별🌠입니다!
이번 포스트에서는 TCA를 활용해 Timer 기능을 구현하는 방법을 알아보겠습니다.
이 예제에서는 사용자가 버튼을 눌러 타이머를 시작 및 정지할 수 있으며, 1초마다 카운트가 증가하는 구조를 다룹니다.
Timer는 일정한 간격으로 동작하는 반복적인 이벤트입니다. iOS에서는 DispatchSourceTimer, Timer, CADisplayLink 등을 사용해 구현할 수 있지만, TCA에서는 Effect와 continuousClock을 활용해 간결하게 처리할 수 있습니다.
타이머가 실행되는 동안 사용자가 화면을 벗어나거나 타이머를 중지해야 할 경우, 불필요한 이벤트가 계속 발생하지 않도록 .cancellable(id:)을 사용해 정리할 수 있습니다.
@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:)을 활용해 중복 실행 방지 및 정리.
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로 타이머를 직접 구현해 보세요! ⏳🚀