VesselWheel

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

Xcode Study

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

JasonYang 2024. 2. 27. 21:57

Timer 클래스를 활용해서, 러닝기록 타이머를 구현하려 시도했다. 러닝기록 타이머 만들기(1/3, 2/3)의 글을 통해서 시도하였을 때, 옵셔널 체이닝 방식으로 MyTimer 클래스에서 타이머를 RunLoop에 등록해서 Main thread에서 해당 매소드가 구동되게금 구현하였다.

하지만, Timer 클래스에는 일시정지하는 매소드나 재실행하는 매소드가 없어서 MyTimer 클래스의 코드의 로직이 부족하다. 

따라서 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.apple.com

요약하자면 아래와 같다. 

더보기

Swift의 `DispatchSource` 클래스는 Grand Central Dispatch(GCD)의 일부로, 애플리케이션의 특정 종류의 시스템 이벤트를 비동기적으로 처리할 수 있는 객체를 제공합니다. 이를 통해 입출력, 타이머, 프로세스 관련 이벤트, 신호, Mach 포트 등 다양한 유형의 시스템 이벤트를 감시하고 이에 대응하는 동작을 수행할 수 있습니다. `DispatchSource`의 주요 특징은 다음과 같습니다:

 

1. **비동기 처리**: `DispatchSource`는 비동기 처리를 지원하므로, 주어진 이벤트를 처리하는 동안 애플리케이션의 나머지 부분이 차단되지 않습니다.

2. **이벤트 핸들링**: `DispatchSource`는 특정 이벤트가 발생할 때마다 실행할 클로저를 설정할 수 있습니다. 이 클로저는 이벤트 핸들러로, 이벤트가 발생할 때마다 호출됩니다.

3. **생명주기 관리**: `DispatchSource`는 `resume()`, `suspend()`, `cancel()` 등의 메서드를 제공하여 dispatch source의 생명주기를 관리할 수 있습니다. 이들 메서드를 통해 dispatch source를 시작하거나 일시 중지하거나 취소할 수 있습니다.

4. **다양한 이벤트 타입 지원**: `DispatchSource`는 다양한 유형의 dispatch source를 생성할 수 있습니다. 예를 들어, `DispatchSource.makeTimerSource()` 메서드는 주기적인 시간 간격으로 이벤트를 발생시키는 타이머 dispatch source를 생성합니다. 따라서 `DispatchSource`는 시스템 이벤트를 비동기적으로 처리하고 이에 대응하는 동작을 수행하는 데 유용한 도구입니다.

-> 비동기로 타이머가 가동되더라도, 앱의 다른 부분이 차단되지 않는다. 

-> 생명주기 관리, 러닝기록의 생명주기인 일시정지, 재시작, 취소를 지원한다. 


dispatchsource의 내부 매소드 중에는 타입매소드인 makeTimerSource가 있다. 

https://developer.apple.com/documentation/dispatch/dispatchsource/2300106-maketimersource

 

makeTimerSource(flags:queue:) | Apple Developer Documentation

Creates a new dispatch source object for monitoring timer events.

developer.apple.com

-> 요약하자면 아래와 같다. 

더보기

`makeTimerSource(flags:queue:)` 메서드는 `DispatchSource`의 타입 메서드로, 타이머 기반의 DispatchSource를 생성하는데 사용됩니다. 이 메서드는 주기적으로 이벤트를 발생시키는 타이머를 생성하며, 이를 통해 특정 시간 간격으로 작업을 수행할 수 있습니다. 이 메서드의 매개변수는 다음과 같습니다:

1. `flags`: 타이머의 동작을 제어하는 옵션입니다. 이 매개변수는 `DispatchSource.TimerFlags` 타입이며, 현재는 `.strict` 하나의 옵션만 제공됩니다. `.strict` 옵션은 타이머가 가능한 한 정확하게 시간을 유지하도록 요구합니다.

 

2. `queue`: 이벤트 핸들러가 실행되는 디스패치 큐를 지정합니다. 이 매개변수는 `DispatchQueue` 타입입니다. 큐를 지정하지 않으면 시스템이 디스패치 큐를 생성하여 사용합니다. `makeTimerSource(flags:queue:)` 메서드를 호출하면, 생성된 타이머는 초기에 일시 중지 상태입니다. `resume()` 메서드를 호출하여 타이머를 시작해야 합니다.

 

또한 `setEventHandler(handler:)` 메서드를 사용하여 이벤트 핸들러를 설정하고, `schedule(deadline:repeating:leeway:)` 메서드를 사용하여 타이머의 시작 시간, 반복 시간, 그리고 유연성을 설정할 수 있습니다. 이렇게 설정한 후에 `resume()` 메서드를 호출하면 타이머가 시작됩니다.

여기에서 Queue 란?

더보기

`queue`(큐)는 컴퓨터 과학에서 매우 중요한 자료 구조 중 하나로, 첫 번째로 들어온 요소가 첫 번째로 나가는 FIFO(First-In-First-Out) 정책을 따릅니다.

 

여기서 말하는 `DispatchQueue`는 Swift의 Grand Central Dispatch(GCD)에서 제공하는 클래스로, 작업을 비동기적으로 수행할 수 있는 대기열을 나타냅니다. `DispatchQueue`는 시스템이 관리하는 스레드 풀에서 작업을 수행하므로, 개발자가 직접 스레드를 관리할 필요가 없습니다. `DispatchQueue`는 크게 두 가지 유형이 있습니다:

 

- `Main Queue`: 메인 스레드에서 작업을 수행하는 대기열입니다. 사용자 인터페이스와 관련된 모든 작업은 메인 큐에서 수행해야 합니다.

- `Global Queue`: 백그라운드에서 작업을 수행하는 대기열입니다. 긴 시간이 걸리는 작업이나 계산 집약적인 작업을 비동기적으로 수행할 때 사용합니다.

 

`DispatchQueue`의 `async` 메서드를 사용하면, 주어진 클로저를 비동기적으로 수행할 수 있습니다. 이 메서드는 클로저를 큐에 추가하고, 큐가 해당 클로저를 가능한 한 빨리 수행하도록 합니다. 이렇게 하면 애플리케이션의 메인 스레드를 차단하지 않고 긴 시간이 걸리는 작업을 수행할 수 있습니다.

-> dispatchsource의 내부매소드는 다음과 같다.

Swift의 `DispatchSource` 클래스는 다양한 메서드를 가지고 있습니다. 주요 메서드들에 대해 설명하겠습니다. 

1. `resume()`: 이 메서드는 일시 중지된 dispatch source를 재개합니다. dispatch source가 생성된 직후에는 일시 중지 상태이므로, 이 메서드를 호출하여 dispatch source를 시작해야 합니다. 

 

2. `suspend()`: 이 메서드는 dispatch source를 일시 중지시킵니다. 이 메서드를 호출하면 dispatch source의 이벤트 핸들러가 더 이상 호출되지 않습니다. 

 

3. `cancel()`: 이 메서드는 dispatch source를 취소합니다. 이 메서드를 호출하면 dispatch source의 이벤트 핸들러가 더 이상 호출되지 않습니다. 취소된 dispatch source는 재사용할 수 없습니다. 

 

4. `setEventHandler(handler:)`: 이 메서드는 dispatch source에서 발생하는 이벤트를 처리할 핸들러를 설정합니다. 핸들러는 클로저 형태로 제공하며, 이벤트가 발생할 때마다 호출됩니다. 

 

5. `setCancelHandler(handler:)`: 이 메서드는 dispatch source가 취소될 때 호출할 핸들러를 설정합니다. 핸들러는 클로저 형태로 제공하며, dispatch source가 취소될 때 한 번만 호출됩니다. `DispatchSource` 클래스는 다양한 유형의 dispatch source를 생성할 수 있는 여러 가지 정적 메서드도 제공합니다. 

예를 들어, `DispatchSource.makeTimerSource()` 메서드는 주기적인 시간 간격으로 이벤트를 발생시키는 타이머 dispatch source를 생성합니다. 위의 메서드 외에도 `DispatchSource` 클래스는 `isCancelled`, `activate()`, `add(data:)` 등의 메서드와 속성을 제공하여 dispatch source의 동작을 더욱 세밀하게 제어할 수 있습니다.


-> Timer 클래스의 내부 매소드인 fire()가 없기 때문에, 타이머를 실행하기 위한 순서는 다음과 같다.

더보기

`DispatchSource` 클래스에는 직접적으로 타이머를 실행하는 메서드는 없습니다. 대신, `DispatchSource`를 사용하여 타이머를 생성할 때는 `makeTimerSource(flags:queue:)` 메서드를 사용하고, 이후에는 `schedule(deadline:repeating:leeway:)` 메서드를 사용하여 타이머의 시작 시간, 반복 시간, 그리고 유연성을 설정한 후, `resume()` 메서드를 호출하여 타이머를 시작합니다. 즉, `DispatchSource`를 사용하여 타이머를 제어할 때는 다음과 같은 순서로 동작을 수행합니다: 

 

1. `makeTimerSource(flags:queue:)` 메서드로 타이머를 생성합니다. 

2. `setEventHandler(handler:)` 메서드로 이벤트 핸들러를 설정합니다. 

3. `schedule(deadline:repeating:leeway:)` 메서드로 타이머의 시작 시간, 반복 시간, 그리고 유연성을 설정합니다. 

4. `resume()` 메서드로 타이머를 시작합니다. 이와 같은 방식으로 `DispatchSource`를 사용하면, 시스템 이벤트를 비동기적으로 처리하고 주기적인 작업을 수행하는 등의 다양한 기능을 구현할 수 있습니다.

 

이제, 코드 단으로 가서 자세히 살펴보자. 

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

import UIKit
import CoreLocation
import Dispatch

class MyTimer {
    var timer: DispatchSourceTimer?
    
    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)?
    
    var backgroundTask: UIBackgroundTaskIdentifier = .invalid
    
    init(time: Int, distance: Double, pace: Double, updateUI: @escaping () -> Void) {
        self.time = time
        self.distance = distance
        self.pace = pace
        self.updateUI = updateUI
        startTimer()
    }
    
    func timerFired() {
        DispatchQueue.main.async {
            self.time += 1
            self.distance += 0.01
            self.pace = self.distance >= 0.05 ? Double(self.time) / self.distance : 0
            
//            print("Timer fired with time: \(self.time), distance: \(self.distance), pace: \(self.pace)")
            
            self.updateUI?()
        }
    }
    public func startTimer() {
        self.beginBackgroundUpdateTask()
        let queue = DispatchQueue.global()
        timer = DispatchSource.makeTimerSource(queue: queue)
        timer?.schedule(deadline: .now(), repeating: .seconds(1))
        timer?.setEventHandler(handler: { [weak self] in
            self?.timerFired()
        })
        timer?.resume()
    }
    
    public func pauseTimer() {
        timer?.suspend()
        
        pausedTime = time
        pausedDistance = distance
        pausedPace = pace
        
    }

    public func resumeTimer() {
        time = pausedTime
        distance = pausedDistance
        pace = pausedPace
        
        print("Resuming timer with time: \(time), distance: \(distance), pace: \(pace)")

        timer?.resume()
    }
    
    public func stopTimer() {
        timer?.cancel()
        timer = nil
        endBackgroundUpdateTask()
    }
    
    private func beginBackgroundUpdateTask() {
        self.backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
            self?.endBackgroundUpdateTask()
        }
    }

    private func endBackgroundUpdateTask() {
        UIApplication.shared.endBackgroundTask(self.backgroundTask)
        self.backgroundTask = .invalid
    }

}

-> 간단 요약하자면 아래와 같다. 

더보기

`MyTimer` 클래스는 `DispatchSourceTimer`를 활용한 사용자 정의 타이머 클래스입니다.

1. `var timer: DispatchSourceTimer?`: 실제 타이머를 저장하는 변수입니다.

2. `init(time: Int, distance: Double, pace: Double, updateUI: @escaping () -> Void)`: 초기화 메서드로, 타이머를 시작할 때 시간, 거리, 페이스를 설정하고 UI 업데이트를 위한 클로저를 받습니다.

3. `func timerFired()`: 타이머가 작동할 때마다 호출되는 메서드입니다. 이 메서드에서는 시간을 1씩 증가시키고, 거리를 0.01씩 증가시키며, 페이스를 계산합니다.

4. `public func startTimer()`: 타이머를 시작하는 메서드입니다. 배경 작업을 시작하고 타이머를 설정한 후, 타이머를 시작합니다.

5. `public func pauseTimer()`: 타이머를 일시 중지하는 메서드입니다. 현재의 시간, 거리, 페이스를 저장하고 타이머를 일시 중지합니다.

6. `public func resumeTimer()`: 타이머를 재개하는 메서드입니다. 일시 중지된 시간, 거리, 페이스를 복원하고 타이머를 재개합니다.

7. `public func stopTimer()`: 타이머를 중지하는 메서드입니다. 타이머를 취소하고 배경 작업을 종료합니다.

8. `private func beginBackgroundUpdateTask()`, `private func endBackgroundUpdateTask()`: 이 두 메서드는 앱이 백그라운드에서 실행될 때 타이머를 계속 작동시키기 위한 메서드입니다. `beginBackgroundUpdateTask()`는 백그라운드 작업을 시작하고, `endBackgroundUpdateTask()`는 백그라운드 작업을 종료합니다. 이 클래스를 사용하면 타이머를 시작, 일시 중지, 재개, 중지하는 등의 기능을 수행할 수 있습니다. 또한, 타이머가 작동할 때마다 UI를 업데이트하는 기능도 제공합니다.

8 번을 통해 앱이 백그라운드 상태에서도 타이머가 계속 동작되게금 하려했으나, 애플 정책상 Appdelegate에 등록하지 않으면 30초로 제한을 둔다고 한다. 

따라서, background tasks 에 대해서 연구하고자 한다. (다음 글 참조) -> https://vesselwheel.tistory.com/203

Using background tasks to update your app

https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app

 

Using background tasks to update your app | Apple Developer Documentation

Configure your app to perform tasks in the background to make efficient use of processing time and power.

developer.apple.com

 


관련 문법 : 비동기프로그래밍

https://teamsparta.notion.site/53e48dede1e44244bdd6e014e9c5e03a

 

비동기 프로그래밍 | Notion

학습목표

teamsparta.notion.site

스택과 큐 

https://teamsparta.notion.site/1-Swift-2154b73eacef4d46b7b3240284804ad1#7e33be3d06124f2eb4a42f7b814c7d12

 

[내일배움캠프] 1주차 : Swift 문법 기본 | Notion

[목차]

teamsparta.notion.site

 

 

다른 코드 풀이

 

    
    var distance: Double = 0
    var time: Int = 0
    var pace: Double = 0

    var timer: Timer?

-> ViewController에서 러닝기록 프로퍼티(distance, time, pace)를 초기화하고, iOS에서 지원하는 Timer 클래스를 채택

    private func recordRunning() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            DispatchQueue.global(qos: .background).async {
                self.time += 1
                self.distance += 0.01 // RunningTimerManager과 연계해서 변경 필요
                // 거리가 0.05km, 즉 50m 이상일 때만 페이스를 계산
                self.pace = self.distance >= 0.05 ? Double(self.time) / self.distance : 0
                
                DispatchQueue.main.async {
                    self.updateTimerUI()
                }
            }
        }
    }

scheduledTimer(withTimeInterval: 1, repeats: true)

-> 버튼 클릭함에 따라 recordRunning()매소드가 실행되고, Timer 클래스의 타입 매소드를 사용하여 .background 쓰레드에서 운용된 모습 

=> TimerManager 커스텀 클래스에서 DispatchSourceTimer를 사용하면 뷰에서는 TimerManager의 .start() 타입 매소드를 사용할 예정이다. 


더보기
import Foundation

class RunningTimer {
    private var timer: DispatchSourceTimer?
    private var startTime: DispatchTime = .now()

    var time: Int = 0
    var distance: Double = 0.0
    var pace: Double = 0.0

    // UI 업데이트를 위한 클로져
    var updateUI: (() -> Void)?

    func start() {
        timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
        timer?.schedule(deadline: .now(), repeating: .seconds(1))
        timer?.setEventHandler { [weak self] in
            guard let self = self else { return }

            self.time += 1
            self.distance += 0.01 // RunningTimerManager와 연계해서 변경 필요
            // 거리가 0.05km, 즉 50m 이상일 때만 페이스를 계산
            self.pace = self.distance >= 0.05 ? Double(self.time) / self.distance : 0

            DispatchQueue.main.async {
                // updateUI 클로져를 호출
                self.updateUI?()
            }
        }
        startTime = .now()
        timer?.resume()
    }

    func pause() {
        timer?.suspend()
    }

    func resume() {
        timer?.resume()
    }

    func stop() {
        timer?.cancel()
        timer = nil
    }
}

 

let runningTimer = RunningTimer()
runningTimer.updateUI = {
    // 여기에 UI 업데이트 코드를 작성하세요.
}
runningTimer.start()

-> ViewController에서의 예시