Timer vs DispatchSourceTimer
Timer의 기능을 활용 하는 방법 2가지, Timer와 DispatchSourceTimer에 대해 알아보겠습니다
Timer
특정 시간 간격이 경과한 후 실행되어 지정된 메시지를 대상 개체로 보냅니다.
"Timers work in conjunction with run loops"main run loop
는 Timer에 대한 강력한 참조를 유지하므로 실행 루프를 추가한 후 참조를 유지할 필요가 없습니다.
main thread는 앱이 포그라운드에 있을 때 동작합니다.
- long run loop callout
장기 실행 루프 콜아웃 - while the run loop is in a mode that isn't monitoring the timer
타이머를 모니터링하지 않는 모드
Timer는 실시간 메커니즘이 아니며, 위와 같은 경우에는 Timer가 실행되는 실제 시간이 훨씬 더 나중일 수 있습니다.
Timer Tolerance
iOS 7 이상에서는 타이머에 대한 허용 오차 tolerance
를 지정할 수 있습니다.
반복 타이머의 경우 다음 시작 날짜가 밀리는 것을 방지하기 위해 개별적인 시작 시간에 적용된 허용 오차에 관계없이 기존 시작 날짜에서 계산됩니다.
기본값은 0으로 오차를 허용하지 않습니다.
일반적인 규칙으로 반복 타이머에 대해 허용 오차의 간격의 10% 이상으로 설정하는 것입니다.
타이머가 작동할 때 시스템 유연성을 허용하면 전력 절감 및 응답성 향상을 위해 시스템을 최적화할 수 있는 능력이 향상됩니다.
Run Loop Modes
관찰할 수 있는 화면 터치와 같은 입력 소스와 타이머의 모음일 뿐만 아니라 이벤트가 발생할 때 알림을 받을 Run Loop observers 입니다.
- default: 입력 소스를 처리
- common: source, timer 및 observer collection을 정의할 수 있는 실행 루프 모드 집합을 처리
- traking: 앱의 반응형 UI를 처리
var timer: Timer?
var time = 0
// closure 내부에서 코드 구현
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { Timer _
self.time += 1
}
// RunLoop에서 타이머를 정의할 수 있도록 처리
RunLoop.current.add(timer, forMode: .common)
// selector 메서드를 통한 메시지 전달
timer = Timer.scheduledTimer(timeIntervalL 1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
@objc func updateTime() {
time += 1
}
타이머 중지
timer?.invalidate()
timer = nil
DispatchSourceTimer
타이머를 기반으로 event handler를 제출하는 DispatchSource
class func makeTimerSource(
flags: DispatchSource.TimerFlags = [],
queue: DispatchQueue? = nil
) -> DispatchSourceTimer
// 구현
let timer = DispatchSource.makeTimerSource(queue: .global(qos: .background))
Testing a DispatchSourceTimer
10 밀리초에 한 번씩 event을 동작시켰습니다.background
로 가더라도 계속 실행되는 것을 확인할 수 있었습니다.
- Simulator에서는 테스트
가 불가능(할 줄 알았으나 동작 가능) - 핸드폰에 연결하여 테스트를 진행
테스트에서 사용된 코드
// ViewController
class ViewController: UIViewController {
private let label = TimeLabel()
private let start = UIButton()
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource(queue: .global(qos: .background))
t.schedule(deadline: .now(), repeating: .milliseconds(10))
t.setEventHandler {
self.time += 1
}
return t
}()
private var time = 0 {
didSet {
label.updateTime(time)
}
}
override func viewDidLoad() {
super.viewDidLoad()
setUI()
setLayout()
}
private func setUI() {
view.backgroundColor = .systemBackground
start.configuration = .filled()
start.configuration?.cornerStyle = .capsule
start.configuration?.baseBackgroundColor = .systemPink
start.configuration?.title = "Start"
start.configuration?.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
var outgoing = incoming
outgoing.foregroundColor = UIColor.white
outgoing.font = UIFont.boldSystemFont(ofSize: 30)
return outgoing
}
start.addTarget(self, action: #selector(tappedButton), for: .touchDown)
}
private func setLayout() {
view.addSubview(label)
view.addSubview(start)
label.translatesAutoresizingMaskIntoConstraints = false
start.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.widthAnchor.constraint(equalToConstant: 150),
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
start.widthAnchor.constraint(equalToConstant: 150),
start.heightAnchor.constraint(equalToConstant: 60),
start.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -150),
start.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
@objc private func tappedButton(_ sender: UIButton) {
sender.isSelected.toggle()
if sender.isSelected {
sender.configuration?.title = "Pause"
timer.resume()
} else {
sender.configuration?.title = "Start"
timer.suspend()
}
}
}
// TimeLabel
class TimeLabel: UILabel {
init() {
super.init(frame: .zero)
textAlignment = .center
text = "00:00"
font = .systemFont(ofSize: 30, weight: .bold)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateTime(_ time: Int) {
let minutes = time / 100
let seconds = time % 100
DispatchQueue.main.async {
let time = String(format: "%02d:%02d", minutes, seconds)
self.text = time
print(time)
}
}
}
Simulator를 통한 테스트
하지만 여기서 문제점은 비동기로 처리하다보니, 어떻게 쓰이냐에 따라 NSLock
과 같이 접근 제어를 할 수 있도록 코드를 구현해야 될 거 같습니다.
개인적으로 두 개의 차이를 표현한다면,
- 앱 사용중에만 Timer가 동작하는 것이라면 Timer로 구현
- Nike Run과 같이 핸드폰 화면이 꺼져도 Timer가 돌아가려면 DispatchSourceTimer로 구현
Background Modes
Xcode 내에서 일반적으로 앱은 백그라운드에 있을 때 일시 중단된 상태입니다.
그러나 앱이 백그라운드에서 실행할 수 있도록 지원하는 백그라운드 실행 모드는 제한되어 있습니다.
이러한 모드 중 하나 이상을 채택하는 앱의 경우 시스템은 백그라운드에서 앱을 시작하거나 다시 시작하고 관련 이벤트를 처리할 시간을 제공합니다.
백그라운드 모드 지정
- Xcode의 `Project navigator` 에서 프로젝트를 선택
- 대상 목록 `Targets list` 에서 앱의 대상을 선택
- 프로젝트 편집기에서 `Signing & Capabilities` 탭을 클릭
- 백그라운드 모드 기능 `Capability` 을 찾습니다
- 하나 이상의 백그라운드 실행 모드를 선택
Audio, AirPlay, and Picture in Picture
을 선택하게 되면 위와 같이 Simulator에서 동작하는 것을 확인할 수 있습니다.
추가적으로 Combine Publisher를 이용한 Timer 구현
Timer.publish(every: TimeInterval, tolerance: TimeInterval?, on: RunLoop, in: RunLoop.Mode, options: RunLoop.schedulerOptions?) -> Timer.TimerPublisher
TimerPublisher: 지정된 간격으로 현재 날짜를 반복적으로 내보내는 게시자
Reference
공식 문서
참고 블로그
'Language > Swift' 카테고리의 다른 글
[Swift] 알고리즘을 대비한 메서드들 (0) | 2024.02.18 |
---|---|
[Swift] UIImage, CGImage 그리고 CIImage 언제 사용되나 (0) | 2024.02.14 |
[Swift] AttributedString (0) | 2023.11.16 |
[Swift] UIButton Configuration (0) | 2023.11.15 |
[Swift] AutoLayout 오토레이아웃 변경 (0) | 2023.11.07 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!