Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- RxCocoa
- UITextView
- map
- Refactoring
- 리팩토링
- Protocol
- collectionview
- uitableview
- combine
- SWIFT
- MVVM
- 리펙터링
- 스위프트
- uiscrollview
- UICollectionView
- Xcode
- swift documentation
- tableView
- Clean Code
- ios
- HIG
- ribs
- 애니메이션
- clean architecture
- rxswift
- Human interface guide
- 리펙토링
- 클린 코드
- swiftUI
- Observable
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 2. Timer 구현하기 - DispatchSourceTimer로 구현 방법 (Background에서도 동작) 본문
iOS 응용 (swift)
[iOS - swift] 2. Timer 구현하기 - DispatchSourceTimer로 구현 방법 (Background에서도 동작)
jake-kim 2021. 11. 24. 03:141. Timer 구현하기 - UIDatePicker 개념, Timer로 구현 방법
2. Timer 구현하기 - DispatchSourceTimer로 구현 방법 (Background에서도 동작)
3. Timer 구현하기 - CircularProgressBar UI 구현(CAShapeLayer 사용), DispatchSourceTimer와 Timer로 구현 방법
UIDatePicker UI
- UIDatePicker UI 개념: https://ios-development.tistory.com/773
- UI 코드
import UIKit
class ViewController: UIViewController {
lazy var countDownDatePicker: UIDatePicker = {
let picker = UIDatePicker()
picker.datePickerMode = .countDownTimer
return picker
}()
lazy var buttonContainerStackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 16
return view
}()
lazy var startRepeatTimerButton: UIButton = {
let button = UIButton()
button.setTitle("타이머 시작", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
button.setTitleColor(.blue, for: .highlighted)
return button
}()
lazy var resumeRepeatTimerButton: UIButton = {
let button = UIButton()
button.setTitle("타이머 재개", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
button.setTitleColor(.blue, for: .highlighted)
button.isHidden = true
return button
}()
lazy var suspendTimerButton: UIButton = {
let button = UIButton()
button.setTitle("일시정지", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
button.setTitleColor(.blue, for: .highlighted)
button.isHidden = true
return button
}()
lazy var cancelTimerButton: UIButton = {
let button = UIButton()
button.setTitle("취소", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
button.setTitleColor(.blue, for: .highlighted)
return button
}()
lazy var countDownLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.font = .systemFont(ofSize: 24)
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
addSubviews()
setupLayout()
}
private func setupViews() {
view.backgroundColor = .systemBackground
}
private func addSubviews() {
view.addSubview(countDownDatePicker)
view.addSubview(buttonContainerStackView)
buttonContainerStackView.addArrangedSubview(startRepeatTimerButton)
buttonContainerStackView.addArrangedSubview(resumeRepeatTimerButton)
buttonContainerStackView.addArrangedSubview(suspendTimerButton)
buttonContainerStackView.addArrangedSubview(cancelTimerButton)
view.addSubview(countDownLabel)
}
private func setupLayout() {
countDownDatePicker.translatesAutoresizingMaskIntoConstraints = false
countDownDatePicker.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
countDownDatePicker.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
buttonContainerStackView.translatesAutoresizingMaskIntoConstraints = false
buttonContainerStackView.topAnchor.constraint(equalTo: countDownDatePicker.bottomAnchor, constant: 24).isActive = true
buttonContainerStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
countDownLabel.translatesAutoresizingMaskIntoConstraints = false
countDownLabel.topAnchor.constraint(equalTo: buttonContainerStackView.bottomAnchor, constant: 24).isActive = true
countDownLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
@objc func didTapRepeatTimerButton() {
}
@objc func didTapResumeRepeatTimerButton() {
}
@objc func didTapSuspendTimerButton() {
}
@objc func didTapCancelTimerButton() {
}
}
DispatchSourceTimer를 이용한 Timer 모듈 구현
- interface 정의
- timerState: 타이머의 상태에 따라 UI버튼에 변화를 주기 위한 값
- start(): 타이머 시작
- resume(): 타이머 재개
- suspend(): 타이머 일시 중지
- cancel(): 타이머 취소 (타이머 다시 시작 가능)
enum TimerState {
case suspended
case resumed
case canceled
case finished
}
protocol RepeatingSecondsTimer {
var timerState: TimerState { get }
func start(durationSeconds: Double,
repeatingExecution: (() -> Void)?,
completion: (() -> Void)?)
func resume()
func suspend()
func cancel()
}
Timer 구현
- 아이디어
- DispatchSourceTimer인스턴스 2개 사용
(각각 setEventHandler로 timer가 종료된 경우 실행하는것과 timer가 매초마다 동작해야하는 로직을 위해 2개 필요) - schedule(deadline:): deadline이 되면 최초 한번 실행하는 메소드
- schedule(deadline:repeating:): deadline부터 repeating초마다 실행
- setEventHandler(completion:): 이벤트 처리 등록
- DispatchSourceTimer인스턴스 2개 사용
- interface 구현체 정의
final class RepeatingSecondsTimerImpl: RepeatingSecondsTimer {
}
- 프로퍼티 정의
- timerState: 외부에서 timer의 상태를 확인하여 UI업데이트를 위한 값
- repeatingExecution: 매초마다 실행되는 클로저
- completion: 지정된 시간이 지나서 타이머가 종료된 경우 실행할 클로저
- timers: 매초 실행되는 timer 인스턴스와 마지막에 실행될 timer 인스턴스를 가지고 있는 변수
var timerState = TimerState.suspended
private var repeatingExecution: (() -> Void)?
private var completion: (() -> Void)?
private var timers: (repeatTimer: DispatchSourceTimer?, nonRepeatTimer: DispatchSourceTimer?) = (DispatchSource.makeTimerSource(),
DispatchSource.makeTimerSource())
- start 정의
- 내부적으로 정의한 setTimer(), resume() 메소드를 호출
- setTimer()에서는 timer객체에 시간과 completion 할당
- resume()에서는 timer 가동
- 내부적으로 정의한 setTimer(), resume() 메소드를 호출
func start(durationSeconds: Double,
repeatingExecution: (() -> Void)? = nil,
completion: (() -> Void)? = nil) {
setTimer(durationSeconds: durationSeconds,
repeatingExecution: repeatingExecution,
completion: completion)
resume()
}
- setTimer(durationSeconds:repeatingExecution:completion:)
- 핵심 코드이며, 지속적으로 실행되는 것과 타이머 종료 시 실행되는 것 두 기능을 위해 두 가지 Timer 인스턴스 사용
private func setTimer(durationSeconds: Double,
repeatingExecution: (() -> Void)? = nil,
completion: (() -> Void)? = nil) {
initTimer()
self.repeatingExecution = repeatingExecution
self.completion = completion
timers.repeatTimer?.schedule(deadline: .now(), repeating: 1)
timers.repeatTimer?.setEventHandler(handler: repeatingExecution)
timers.nonRepeatTimer?.schedule(deadline: .now() + durationSeconds)
timers.nonRepeatTimer?.setEventHandler { [weak self] in
self?.finish()
completion?()
}
}
- 인스턴스가 해제될 때 timer도 같이 해제되도록 설계
deinit {
removeTimer()
}
private func removeTimer() {
// cancel()을 한번 실행하면 timer를 다시 사용할 수 없는 상태임을 주의
// cancel()을 할땐 resume()을 호출한 후 해야 크래시가 나지 않고 정상 취소가 가능
timers.repeatTimer?.resume()
timers.nonRepeatTimer?.resume()
timers.repeatTimer?.cancel()
timers.nonRepeatTimer?.cancel()
initTimer()
}
private func initTimer() {
timers.repeatTimer?.setEventHandler(handler: nil)
timers.nonRepeatTimer?.setEventHandler(handler: nil)
repeatingExecution = nil
completion = nil
}
- 재개 / 일시정지 / 취소 / 종료
func resume() {
guard timerState == .suspended else { return }
timerState = .resumed
timers.repeatTimer?.resume()
timers.nonRepeatTimer?.resume()
}
func suspend() {
guard timerState == .resumed else { return }
timerState = .suspended
timers.repeatTimer?.suspend()
timers.nonRepeatTimer?.suspend()
}
func cancel() {
timerState = .canceled
initTimer()
}
private func finish() {
timerState = .finished
cancel()
}
- 전체 RepeatingTimerImpl 코드
import Foundation
enum TimerState {
case suspended
case resumed
case canceled
case finished
}
protocol RepeatingSecondsTimer {
var timerState: TimerState { get }
func start(durationSeconds: Double,
repeatingExecution: (() -> Void)?,
completion: (() -> Void)?)
func resume()
func suspend()
func cancel()
}
final class RepeatingSecondsTimerImpl: RepeatingSecondsTimer {
var timerState = TimerState.suspended
private var repeatingExecution: (() -> Void)?
private var completion: (() -> Void)?
private var timers: (repeatTimer: DispatchSourceTimer?, nonRepeatTimer: DispatchSourceTimer?) = (DispatchSource.makeTimerSource(),
DispatchSource.makeTimerSource())
deinit {
removeTimer()
}
func start(durationSeconds: Double,
repeatingExecution: (() -> Void)? = nil,
completion: (() -> Void)? = nil) {
setTimer(durationSeconds: durationSeconds,
repeatingExecution: repeatingExecution,
completion: completion)
resume()
}
func resume() {
guard timerState == .suspended else { return }
timerState = .resumed
timers.repeatTimer?.resume()
timers.nonRepeatTimer?.resume()
}
func suspend() {
guard timerState == .resumed else { return }
timerState = .suspended
timers.repeatTimer?.suspend()
timers.nonRepeatTimer?.suspend()
}
func cancel() {
timerState = .canceled
initTimer()
}
private func finish() {
timerState = .finished
cancel()
}
private func setTimer(durationSeconds: Double,
repeatingExecution: (() -> Void)? = nil,
completion: (() -> Void)? = nil) {
initTimer()
self.repeatingExecution = repeatingExecution
self.completion = completion
timers.repeatTimer?.schedule(deadline: .now(), repeating: 1)
timers.repeatTimer?.setEventHandler(handler: repeatingExecution)
timers.nonRepeatTimer?.schedule(deadline: .now() + durationSeconds)
timers.nonRepeatTimer?.setEventHandler { [weak self] in
self?.finish()
completion?()
}
}
private func initTimer() {
timers.repeatTimer?.setEventHandler(handler: nil)
timers.nonRepeatTimer?.setEventHandler(handler: nil)
repeatingExecution = nil
completion = nil
}
private func removeTimer() {
// cancel()을 한번 실행하면 timer를 다시 사용할 수 없는 상태임을 주의
// cancel()을 할땐 resume()을 호출한 후 해야 크래시가 나지 않고 정상 취소가 가능
timers.repeatTimer?.resume()
timers.nonRepeatTimer?.resume()
timers.repeatTimer?.cancel()
timers.nonRepeatTimer?.cancel()
initTimer()
}
}
사용하는 쪽
- ViewController 준비
class ViewController: UIViewController {
}
- repeatingSecondsTimer 모듈 선언
private let repeatingSecondsTimer: RepeatingSecondsTimer
init(repeatingSecondsTimer: RepeatingSecondsTimer) {
self.repeatingSecondsTimer = repeatingSecondsTimer
super.init(nibName: nil, bundle: nil)
}
- 타이머 시작 버튼을 누른 경우 실행되는 메소드 정의
- 타이머가 실행중인 경우에는 +1씩 UI 업데이트
- 타이머가 종료되었을 때는 "타이머 완료" UI 업데이트
var time = 0
private func startRepeatTimer() {
repeatingSecondsTimer.start(durationSeconds: countDownDatePicker.countDownDuration) {
DispatchQueue.main.async {
self.time += 1
self.countDownLabel.text = "타이머 = \(self.time)"
}
} completion: {
DispatchQueue.main.async { [weak self] in
self?.countDownLabel.text = "타이머 완료"
}
}
}
- 각 버튼을 누른 경우 Timer 동작
@objc func didTapRepeatTimerButton() {
startRepeatTimer()
setTimerButtonsUsingTimerState()
}
@objc func didTapResumeRepeatTimerButton() {
repeatingSecondsTimer.resume()
setTimerButtonsUsingTimerState()
}
@objc func didTapSuspendTimerButton() {
repeatingSecondsTimer.suspend()
setTimerButtonsUsingTimerState()
}
@objc func didTapCancelTimerButton() {
repeatingSecondsTimer.cancel()
setTimerButtonsUsingTimerState()
}
private func setTimerButtonsUsingTimerState() {
switch repeatingSecondsTimer.timerState {
case .resumed:
startRepeatTimerButton.isHidden = true
suspendTimerButton.isHidden = false
resumeRepeatTimerButton.isHidden = true
case .suspended:
startRepeatTimerButton.isHidden = true
suspendTimerButton.isHidden = true
resumeRepeatTimerButton.isHidden = false
case .canceled:
startRepeatTimerButton.isHidden = false
suspendTimerButton.isHidden = true
resumeRepeatTimerButton.isHidden = true
countDownLabel.text = "타이머 취소"
time = 0
case .finished: break
}
}
* 전체 소스 코드: https://github.com/JK0369/ExTimerModule
'iOS 응용 (swift)' 카테고리의 다른 글
Comments