본문 바로가기

iOS/SwiftUI

SwiftUI로 Countdown Timer 만들어보기

💫 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