VesselWheel

러닝기록 타이머 만들기(1/3)(with thread, RunLoop) 본문

Xcode Study

러닝기록 타이머 만들기(1/3)(with thread, RunLoop)

JasonYang 2024. 2. 26. 21:59

결론부터 말하자면, dispatchsource 클래스를 활용하여 러닝기록 타이머를 구현한다.

이 글의 마지막으로 내려가서 (3/3) 의 글을 보시고, 이해를 위해서 현재 보고 있는 글을 참고하시라. 


공식문서를 읽기 전에 참고 블로그로 대략적인 흐름에 대해 이해하려했다. 

https://please-amend.tistory.com/entry/Swift-Timer%ED%83%80%EC%9D%B4%EB%A8%B8%EC%99%80-Thread%EC%8A%A4%EB%A0%88%EB%93%9C-RunLoop%EB%9F%B0%EB%A3%A8%ED%94%84

 

[iOS] Timer(타이머)와 Thread(스레드), RunLoop(런루프)

** 아직 공부하는 중이라 틀린 내용이 있을 수도 있습니다. ** 최근 프로젝트에서 반복 타이머가 필요한 경우가 있었는데, 그때 알아보았던 타이머, 스레드, 런루프에 대해 까먹기 전에 정리하려

please-amend.tistory.com

https://please-amend.tistory.com/35?category=1172099

 

[iOS] 백그라운드 스레드에서 타이머 돌리기

** 아직 공부하는 중이라 틀린 내용이 있을 수도 있습니다. ** 저번 글에서는 런루프와 타이머의 관계, 타이머 생성하는 법까지 다뤘다. [Swift] Timer(타이머)와 Thread(스레드), RunLoop(런루프) ** 아직

please-amend.tistory.com


그리고 나서, 다시 공식문서을 읽었다. 

https://developer.apple.com/documentation/foundation/timer

 

Timer | Apple Developer Documentation

A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.

developer.apple.com

러닝기록 타이머를 구현하기 위해서 Timer 클래스의 scheduledTimer를 사용하면 된다고한다. 

https://developer.apple.com/documentation/foundation/timer/1415941-scheduledtimer

 

scheduledTimer(timeInterval:invocation:repeats:) | Apple Developer Documentation

Creates a new timer and schedules it on the current run loop in the default mode.

developer.apple.com

 

또한 

DispatchSourceTimer를 활용해서 타이머를 만들 수 있다고 한다. 

-> 해당 내용은 (3/3)에서 다루고자 한다. 

https://developer.apple.com/documentation/dispatch/dispatchsourcetimer/

 

DispatchSourceTimer | Apple Developer Documentation

A dispatch source that submits the event handler block based on a timer.

developer.apple.com


 

우선 Timer 클래스와 RunLoop에 대해서 알아보았다. 그리고나서, 기능 구현을 진행해나아갔다. 

1. 공식문서에 의하면 Run Loop에 타이머를 등록해야한다. 이 때 사용되는 코드가 아래와 같다. 

scheduledTimer(timeInterval:invocation:repeats:) 혹은
scheduledTimer(timeInterval:target:selector:userInfo:repeats:)

-> 위 2가지 매소드의 차이점은 아래와 같다. 

더보기
  1. scheduledTimer(timeInterval:invocation:repeats:) 메서드:
    • 이 메서드는 NSInvocation 객체를 사용하여 타이머가 트리거될 때 실행되는 메서드를 지정합니다.
    • NSInvocation은 Objective-C 런타임에서 메서드 호출을 나타내는 객체로, 메서드의 선택자(selector), 대상(target), 인자(arguments) 등을 포함하고 있습니다.
    • Swift에서는 NSInvocation을 지원하지 않아, 이 메서드는 Swift에서 사용할 수 없습니다.
  2. scheduledTimer(timeInterval:target:selector:userInfo:repeats:) 메서드:
    • 이 메서드는 대상 객체와 선택자를 직접 지정하여 타이머가 트리거될 때 실행되는 메서드를 지정합니다.
    • target은 메서드가 있는 객체를 나타내고, selector는 실행할 메서드를 나타냅니다.
    • Swift에서는 #selector 문법을 사용하여 메서드를 지정할 수 있습니다.

따라서, Swift에서 타이머를 사용하려면 scheduledTimer(timeInterval:target:selector:userInfo:repeats:) 메서드를 사용해야 합니다.

-> 공식문서를 읽어서는 알 수 없는 내용을, 결국 Chat GPT에게 물어보았다. 

그리하여,

scheduledTimer(timeInterval:target:selector:userInfo:repeats:) 를 사용해보자. 

https://developer.apple.com/documentation/foundation/timer/1412416-scheduledtimer

 

scheduledTimer(timeInterval:target:selector:userInfo:repeats:) | Apple Developer Documentation

Creates a timer and schedules it on the current run loop in the default mode.

developer.apple.com

시작하기 전에 Timer 클래스에는 내부 매소드가 있다.  알아보고 지나가자. 

더보기

Swift의 `Timer` 클래스는 다양한 메서드를 가지고 있습니다. 여기에는 `fire()`, `invalidate()` 등이 포함되어 있습니다.

주요 메서드들에 대해 설명하겠습니다.

1. `fire()`: 이 메서드는 타이머를 즉시 실행합니다. 일반적으로 타이머는 설정된 간격에 따라 실행되지만, `fire()` 메서드를 호출하면 해당 시점에 타이머의 액션을 즉시 실행할 수 있습니다.

2. `invalidate()`: 이 메서드는 타이머를 무효화하고, 더 이상 메시지를 보내지 않도록 합니다. 타이머를 중지하거나 제거할 때 사용합니다.

3. `scheduledTimer(timeInterval:target:selector:userInfo:repeats:)`: 이 메서드는 주어진 시간 간격으로 타이머를 생성하고, 현재의 런루프에 스케줄링합니다. 타이머가 트리거될 때 실행할 메서드를 지정할 수 있습니다.

4. `scheduledTimer(withTimeInterval:repeats:block:)`: 이 메서드는 주어진 시간 간격으로 타이머를 생성하고, 현재의 런루프에 스케줄링합니다. 타이머가 트리거될 때 실행할 클로저를 지정할 수 있습니다.

5. `init(timeInterval:target:selector:userInfo:repeats:)`: 이 메서드는 주어진 시간 간격으로 타이머를 생성하지만, 런루프에 자동으로 추가하지 않습니다. 만들어진 타이머를 수동으로 런루프에 추가해야 합니다.

6. `init(timeInterval:invocation:repeats:)`: 이 메서드는 `NSInvocation` 객체를 사용하여 타이머를 생성합니다. 하지만 Swift에서는 지원하지 않는 기능입니다. 위의 메서드 외에도 `Timer` 클래스는 `timeInterval`, `isValid`, `userInfo` 등의 속성을 제공하여 타이머의 동작을 더욱 세밀하게 제어할 수 있습니다. -> 사용할 수 없다. 

 

더보기
//
//  MyTimer.swift
//  Run-It
//
//  Created by Jason Yang on 2/26/24.
//

import Foundation

class MyTimer {
    var timer: Timer?
    
    var time = 0
    var distance = 0.0
    var pace = 0.0
    
    var pausedTime = 0 // 일시정지된 시간을 저장할 변수
    var pausedDistance = 0.0 // 일시정지된 거리를 저장할 변수
    var pausedPace = 0.0 // 일시정지된 페이스를 저장할 변수
    
    var isPaused = false
    
    var updateUI: (() -> Void)?
    
    init(updateUI: @escaping () -> Void) {
        self.updateUI = updateUI
        startTimer()
    }
    
    @objc func timerFired() {
        // 이 메서드는 타이머가 동작할 때마다 호출됩니다.
        DispatchQueue.main.async {
            self.time += 1
            self.distance += 0.01 // RunningTimerManager과 연계해서 변경 필요
            // 거리가 0.05km, 즉 50m 이상일 때만 페이스를 계산
            self.pace = self.distance >= 0.05 ? Double(self.time) / self.distance : 0
            //            print(self.time)
            //            print(self.distance)
            self.updateUI?() // 상태가 변경될 때마다 UI 업데이트
        }
    }
    public func startTimer() {
        DispatchQueue.main.async {
            self.timer = Timer(timeInterval: 1.0, target: self, selector: #selector(self.timerFired), userInfo: nil, repeats: true)
            RunLoop.current.add(self.timer!, forMode: .common)
            self.timer?.fire()
        }
    }
    
    public func pauseTimer() {
        pausedTime = time // 일시정지 시점의 시간 저장
        pausedDistance = distance // 일시정지 시점의 거리 저장
        pausedPace = pace // 일시정지 시점의 페이스 저장
        
        timer?.invalidate()
    }

    public func resumeTimer() {
        // 일시정지 했던 시점의 시간, 거리, 페이스를 복원
        time = pausedTime
        distance = pausedDistance
        pace = pausedPace

        startTimer()
    }
    
    public func stopTimer() {
        timer?.invalidate()
        timer = nil
    }
}

1. 러닝기록 타이머를 책임질 class MyTimer 를 생성하였다.  - Timer 클래스를 사용하기 위해 인스턴스화 해주고 나서, 

var timer: Timer?

 

2. Timer 클래스에서 이용한 인스턴스를 생성한다. 

    var time = 0
    var distance = 0.0
    var pace = 0.0
    
    var pausedTime = 0 // 일시정지된 시간을 저장할 변수
    var pausedDistance = 0.0 // 일시정지된 거리를 저장할 변수
    var pausedPace = 0.0 // 일시정지된 페이스를 저장할 변수
    
    var isPaused = false
    
    var updateUI: (() -> Void)?

3. MyTimer 클래스가 초기화 될 때, 뷰에서 러닝기록과 관련된 UI 프로퍼티가 최신화 되어야하기 때문에, 클로져함수로 UI를 업데이트하면서 Timer 클래스의 내장 타이머 기능 매소드를 실행해준다. 

    init(updateUI: @escaping () -> Void) {
        self.updateUI = updateUI
        startTimer()
    }

 

4. timerFired()를 통해서 시간, 거리에 따른 페이스가 UI에서 변경되어야하기때문에 Main에서 비동기로 최신화해주고, UI를 업데이트해준다. 

- startTimer()매소드를 통해서 timerFired매소드는 화면이 꺼지더라도 계속 실행되어 러닝기록을 유지해야기 때문에, RunLoop로 current에 .common모드로 타이머를 가동한다. 

- self.timer?.fire()를 통해, 타이머를 작동시켜준다. 

- startTimer()는 view에서 호출하여 UI 버튼과 연계하여 사용한다.

    @objc func timerFired() {
        // 이 메서드는 타이머가 동작할 때마다 호출됩니다.
        DispatchQueue.main.async {
            self.time += 1
            self.distance += 0.01 // RunningTimerManager과 연계해서 변경 필요
            // 거리가 0.05km, 즉 50m 이상일 때만 페이스를 계산
            self.pace = self.distance >= 0.05 ? Double(self.time) / self.distance : 0
            //            print(self.time)
            //            print(self.distance)
            self.updateUI?() // 상태가 변경될 때마다 UI 업데이트
        }
    }
    public func startTimer() {
        DispatchQueue.main.async {
            self.timer = Timer(timeInterval: 1.0, target: self, selector: #selector(self.timerFired), userInfo: nil, repeats: true)
            RunLoop.current.add(self.timer!, forMode: .common)
            self.timer?.fire()
        }
    }
    
    public func pauseTimer() {
        pausedTime = time // 일시정지 시점의 시간 저장
        pausedDistance = distance // 일시정지 시점의 거리 저장
        pausedPace = pace // 일시정지 시점의 페이스 저장
        
        timer?.invalidate()
    }

    public func resumeTimer() {
        // 일시정지 했던 시점의 시간, 거리, 페이스를 복원
        time = pausedTime
        distance = pausedDistance
        pace = pausedPace

        startTimer()
    }
    
    public func stopTimer() {
        timer?.invalidate()
        timer = nil
    }
}

- 추가적으로, 러닝기록을 일시정지, 재실행, 정지 기능을 public으로 view에서 호출할 수 있도록 한다. 

-> Timer 클래스에는 fire() 매소드와 invalidate() 매소드만 존재하기 때문에, 타이머를 재실행하기 위해 resume이나 일시정지하는 pause를 직접구현할 수 없었다. 따라서 커스텀 매소드를 만들어 구현하였다. 

이로써, 코드단에서 다소 복잡하고 코드가 길어진다는 문제점을 발견하였다. 

따라서, dispatchsource 클래스를 활용해서 타이머를 구현해보고자 한다. 

[아래의 이어지는 블로그 참조]


https://vesselwheel.tistory.com/204

 

러닝기록 타이머 만들기(3/3)(with dispatchsource )

https://developer.apple.com/documentation/dispatch/dispatchsource DispatchSource | Apple Developer Documentation An object that coordinates the processing of specific low-level system events, such as file-system events, timers, and UNIX signals. developer.

vesselwheel.tistory.com