💫 Timer를 활용한 방법
struct ContentView: View {
var body: some View {
VStack {
Text("Timer")
TimerCountdownTimer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
}
struct TimerCountdownTimer: View {
@State private var timeRemaining = 10
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
if timeRemaining > 0 {
Text("\(timeRemaining)")
} else {
Text("Time over!")
}
}
.onReceive(timer) { time in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
}
}
Timer를 사용하여 매 1초마다 타이머를 동작시키고 onReceive에서 매 초마다 남은 시간을 1초씩 줄여줘서 타이머를 동작하는 방법
💫 TimelineView를 활용한 방법
iOS 15부터 사용가능한 TimelineView를 사용해서 카운트다운 타이머를 구현할 수 있다.
struct ContentView: View {
var body: some View {
VStack(spacing: 30) {
VStack {
Text("TimelineView")
TimelineCountdownTimer()
}
VStack {
Text("Timer")
TimerCountdownTimer()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
}
struct TimelineCountdownTimer: View {
private let calendar = Calendar.current
var body: some View {
let targetDate = calendar.date(byAdding: .second, value: 10, to: Date())!
TimelineView(.animation) { context in
let timeLimit = calendar.dateComponents([.second, .nanosecond], from: context.date, to: targetDate)
if case let (second?, nanosecond?) = (timeLimit.second, timeLimit.nanosecond),
nanosecond > 0 {
Text("\(second + 1)")
} else {
Text("Time over!")
}
}
}
}
현재보다 10초 후의 시간을 targetDate로 저장하고 TimelineView에서 매 초마다 현재시간을 불러와서 targetDate와 시간 차이를 계산하여 보여주는 방법
상대적으로 약간의 딜레이가 있지만 각각의 타이머는 거의 정확하게 1초마다 갱신되고있다.
❌ 문제점
TimelineView는 최소 지원이 iOS15이므로 그 미만의 버전에서는 사용 할 수가 없다.
그리고 Timer는 main 쓰레드에서 돌아가기 때문에 메인 쓰레드에서 어떤 동작이 같이 실행되면 타이머가 멈추는 현상이 있다.
TextField에 글을 쓸 때마다 main 쓰레드를 사용하기 때문에 그 동안 Timer가 멈추는 현상이 있어서 정확한 시간을 카운팅 할 수가 없게 된다. 이를 해결하기 위해서는 카운팅을 비동기로 해야하는데 Combine을 사용하면 간단하게 구현 할 수 있다.
💫 Combine을 활용한 방법
가장 처음에 기술한 Timer를 Combine을 사용해서 Async하게 동작하게 하는 방법이다.
먼저 ObservableObject 프로토콜을 준수한 TimerViewModel 클래스를 만들어준다.
import Combine
class TimerViewModel: ObservableObject {
// 남은시간
var timeRemaining = 10 {
didSet {
guard timeRemaining > 0 else {
stop()
return
}
message = "\(timeRemaining)"
}
}
// 화면에 보여질 메세지
@Published var message: String = ""
// 타이머
var timer: AnyCancellable?
// 초기화 동시에 타이머를 실행시킨다.
init() {
message = "\(timeRemaining)"
start()
}
// 카운트다운 시작
private func start() {
timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.timeRemaining -= 1 // 👈 1초마다 남은시간을 1씩 빼준다.
}
}
// 카운트다운 종료
private func stop() {
timer?.cancel()
timer = nil
message = "Time over!"
}
}
위에서 작성한 ViewModel의 메세지를 읽어와서 화면에 보여준다.
struct CombineTimerCountdownTimer: View {
@StateObject var timer = TimerViewModel()
var body: some View {
Text("\(timer.message)")
}
}
'iOS > SwiftUI' 카테고리의 다른 글
Alignment Guides (0) | 2022.07.11 |
---|---|
SwiftUI로 CheckBoxTreeView 만들기! (0) | 2022.07.01 |
SwiftUI 그라데이션 텍스트 구현하기 (0) | 2022.04.27 |
SwiftUI에서 UITextField 커스텀하여 사용하기 (0) | 2022.04.10 |
SwiftUI와 상태관리 (0) | 2022.04.03 |