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 |
Tags
- HIG
- 리펙토링
- collectionview
- Refactoring
- MVVM
- RxCocoa
- uiscrollview
- Clean Code
- rxswift
- 애니메이션
- 클린 코드
- map
- 리펙터링
- UICollectionView
- ribs
- uitableview
- 리팩토링
- 스위프트
- Xcode
- swift documentation
- UITextView
- clean architecture
- Protocol
- ios
- swiftUI
- Observable
- combine
- Human interface guide
- SWIFT
- tableView
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 3. Timer 구현하기 - CircularProgressBar UI 구현(CAShapeLayer 사용), DispatchSourceTimer와 Timer로 타이머 구현 방법 본문
iOS 응용 (swift)
[iOS - swift] 3. Timer 구현하기 - CircularProgressBar UI 구현(CAShapeLayer 사용), DispatchSourceTimer와 Timer로 타이머 구현 방법
jake-kim 2021. 11. 25. 23:321. Timer 구현하기 - UIDatePicker 개념, Timer로 구현 방법
2. Timer 구현하기 - DispatchSourceTimer로 구현 방법 (Background에서도 동작)
3. Timer 구현하기 - CircularProgressBar UI 구현(CAShapeLayer 사용), DispatchSourceTimer와 Timer로 타이머 구현 방법
아이디어
- 첫 번째 화면에서 UIDatePicker를 통해 시간을 선택하고 확인을 누르면 먼저 DispatchSourceTimer로 만든 모듈이 동작
- 확인 버튼을 눌렀을때 다음 화면에서 직접 만든 CircularTimerView에 타이머 관련된 정보를 표출
첫 번째 화면 SettinTimerVC는 글을 2. Timer 구현하기 - DispatchSourceTimer로 구현 방법 (Background에서도 동작) 참고
class SettingTimerVC: UIViewController {
private let repeatingSecondsTimer: RepeatingSecondsTimer
private lazy var countDownDatePicker: UIDatePicker = {
let picker = UIDatePicker()
picker.datePickerMode = .countDownTimer
return picker
}()
private lazy var confirmButton: UIButton = {
let button = UIButton()
button.setTitle("확인", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
button.setTitleColor(.blue, for: .highlighted)
button.addTarget(self, action: #selector(didTapConfirmButton), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
addSubviews()
setupLayout()
}
private func setupViews() {
view.backgroundColor = .systemBackground
}
private func addSubviews() {
view.addSubview(countDownDatePicker)
view.addSubview(confirmButton)
}
private func setupLayout() {
countDownDatePicker.translatesAutoresizingMaskIntoConstraints = false
countDownDatePicker.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
countDownDatePicker.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
confirmButton.translatesAutoresizingMaskIntoConstraints = false
confirmButton.topAnchor.constraint(equalTo: countDownDatePicker.bottomAnchor, constant: 56).isActive = true
confirmButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
init(repeatingSecondsTimer: RepeatingSecondsTimer) {
self.repeatingSecondsTimer = repeatingSecondsTimer
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
@objc private func didTapConfirmButton() {
startTimer()
let circularTimerVC = CircularTimerVC(startDate: Date(), countDownDurationSeconds: countDownDatePicker.countDownDuration)
navigationController?.pushViewController(circularTimerVC, animated: true)
}
private func startTimer() {
repeatingSecondsTimer.start(durationSeconds: countDownDatePicker.countDownDuration, repeatingExecution: nil) {
print("완료")
}
}
}
CircularTimerView (CircularProgressBar) UI 구현
- extension 정의
- extension TimeInterval: TimeInterval값을 String값으로 표현 "00:00"
- extension Int: degree 단위를 radian 단위로 변경
extension TimeInterval {
/// %02d: 빈자리를 0으로 채우고, 2자리 정수로 표현
var time: String {
return String(format:"%02d:%02d", Int(self/60), Int(ceil(truncatingRemainder(dividingBy: 60))) )
}
}
extension Int {
var degreesToRadians: CGFloat {
return CGFloat(self) * .pi / 180
}
}
- 원을 그리기 전에 필요한 instance 선언
- startDate: 시작 시간
- leftSeconds: 끝나기까지 남아있는 시간
- endSeconds: 끝나는 시간
- delegate: 타이머가 끝난것을 알려주는 인터페이스
protocol CircularTimerViewDelegate: AnyObject {
func didFinishTimer()
}
struct ProgressColors {
var trackLayerStrokeColor: CGColor = UIColor.lightGray.cgColor
var barLayerStrokeColor: CGColor = UIColor.green.cgColor
}
class CircularTimerView: UIView {
private let progressColors: ProgressColors
private let startDate: Date
private var leftSeconds: TimeInterval
private lazy var timer = Timer()
private lazy var endSeconds = startDate.addingTimeInterval(leftSeconds)
weak var delegate: CircularTimerViewDelegate?
}
- 원을 표출하기 위해서 UIBezierPath를 사용
- path에 관한 곡선, 직선을 그릴 수 있는 인스턴스이며 해당 path를 CAShapeLayer에 입력하면 path 모양이 반영
- 원을 그리는 UIBezierPath();arcCenter:radius:startAngle:endAngle:clockwise:)를 사용하여 원 path모양 생성
private lazy var circularPath: UIBezierPath = {
return UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
radius: 100, // 반지름
startAngle: -90.degreesToRadians, // 12시 방향 (0도가 3시방향)
endAngle: CGFloat.pi * 2, // 2시 방향
clockwise: true)
}()
- CAShapeLayer의 path 프로퍼티에 위에서 만든 UIBezierPath 인스턴스를 입력하여 원 생성
private lazy var trackLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = progressColors.trackLayerStrokeColor
layer.lineWidth = 15
return layer
}()
private lazy var barLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = progressColors.barLayerStrokeColor
layer.lineWidth = 15
return layer
}()
private func addSubviews() {
layer.addSublayer(trackLayer)
layer.addSublayer(barLayer)
}
- 시간정보를 표출할 UILabel 정의
private lazy var timeLabel: UILabel = {
let label = UILabel(frame: CGRect(x: frame.midX - 50,
y: frame.midY - 25,
width: 100,
height: 50))
label.textAlignment = .center
label.textColor = .label
return label
}()
- barLayer에 track을 따라서 그려질 애니메이션 적용
- CABasicAnimation 인스턴스를 생성하여 barLayer에 추가
private func animateToBarLayer() {
let strokeAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0
strokeAnimation.toValue = 1
strokeAnimation.duration = leftSeconds
barLayer.add(strokeAnimation, forKey: nil)
timer = Timer.scheduledTimer(timeInterval: 0.1,
target: self,
selector: #selector(updateTime),
userInfo: nil,
repeats: true)
}
@objc private func updateTime() {
if leftSeconds > 0 {
leftSeconds = endSeconds.timeIntervalSinceNow
timeLabel.text = leftSeconds.time
} else {
timer.invalidate()
timeLabel.text = "00:00"
delegate?.didFinishTimer()
}
}
- 전체 CircularTimerView 코드
import Foundation
import UIKit
protocol CircularTimerViewDelegate: AnyObject {
func didFinishTimer()
}
struct ProgressColors {
var trackLayerStrokeColor: CGColor = UIColor.lightGray.cgColor
var barLayerStrokeColor: CGColor = UIColor.green.cgColor
}
class CircularTimerView: UIView {
private let progressColors: ProgressColors
private let startDate: Date
private var leftSeconds: TimeInterval
private lazy var timer = Timer()
private lazy var endSeconds = startDate.addingTimeInterval(leftSeconds)
weak var delegate: CircularTimerViewDelegate?
private lazy var circularPath: UIBezierPath = {
return UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
radius: 100, // 반지름
startAngle: -90.degreesToRadians, // 12시 방향 (0도가 3시방향)
endAngle: CGFloat.pi * 2, // 2시 방향
clockwise: true)
}()
private lazy var trackLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = progressColors.trackLayerStrokeColor
layer.lineWidth = 15
return layer
}()
private lazy var barLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = progressColors.barLayerStrokeColor
layer.lineWidth = 15
return layer
}()
private lazy var timeLabel: UILabel = {
let label = UILabel(frame: CGRect(x: frame.midX - 50,
y: frame.midY - 25,
width: 100,
height: 50))
label.textAlignment = .center
label.textColor = .label
return label
}()
init(progressColors: ProgressColors, duration: TimeInterval, startDate: Date) {
self.progressColors = progressColors
self.leftSeconds = duration
self.startDate = startDate
super.init(frame: .zero)
addSubviews()
setupViews()
}
required init?(coder: NSCoder) {
fatalError()
}
private func addSubviews() {
layer.addSublayer(trackLayer)
layer.addSublayer(barLayer)
addSubview(timeLabel)
}
private func setupViews() {
animateToBarLayer()
}
private func animateToBarLayer() {
let strokeAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0
strokeAnimation.toValue = 1
strokeAnimation.duration = leftSeconds
barLayer.add(strokeAnimation, forKey: nil)
timer = Timer.scheduledTimer(timeInterval: 0.1,
target: self,
selector: #selector(updateTime),
userInfo: nil,
repeats: true)
}
@objc private func updateTime() {
if leftSeconds > 0 {
leftSeconds = endSeconds.timeIntervalSinceNow
timeLabel.text = leftSeconds.time
} else {
timer.invalidate()
timeLabel.text = "00:00"
delegate?.didFinishTimer()
}
}
}
CircularTimerView를 사용하는쪽
- CircularTimerVC 정의
class CircularTimerVC: UIViewController {
private let countDownDurationSeconds: TimeInterval
private let startDate: Date
init(startDate: Date, countDownDurationSeconds: TimeInterval) {
self.startDate = startDate
self.countDownDurationSeconds = countDownDurationSeconds
super.init(nibName: nil, bundle: nil)
}
}
- circularTimerView 선언
private lazy var circularTimerView: CircularTimerView = {
let progressColors = ProgressColors(trackLayerStrokeColor: UIColor.lightGray.cgColor,
barLayerStrokeColor: UIColor.green.cgColor)
let view = CircularTimerView(progressColors: progressColors,
duration: countDownDurationSeconds,
startDate: startDate)
return view
}()
- 전체 CircularTimerVC 코드
import Foundation
import UIKit
class CircularTimerVC: UIViewController {
private let countDownDurationSeconds: TimeInterval
private let startDate: Date
private lazy var circularTimerView: CircularTimerView = {
let progressColors = ProgressColors(trackLayerStrokeColor: UIColor.lightGray.cgColor,
barLayerStrokeColor: UIColor.green.cgColor)
let view = CircularTimerView(progressColors: progressColors,
duration: countDownDurationSeconds,
startDate: startDate)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
addSubviews()
makeConstraints()
}
init(startDate: Date, countDownDurationSeconds: TimeInterval) {
self.startDate = startDate
self.countDownDurationSeconds = countDownDurationSeconds
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
private func setupViews() {
view.backgroundColor = .systemBackground
}
private func addSubviews() {
view.addSubview(circularTimerView)
}
private func makeConstraints() {
circularTimerView.translatesAutoresizingMaskIntoConstraints = false
circularTimerView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
circularTimerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
}
* 전체 소스 코드: https://github.com/JK0369/ExCircularTimer
* 참고
- https://developer.apple.com/documentation/quartzcore/cabasicanimation
- https://developer.apple.com/documentation/quartzcore/cashapelayer
- https://developer.apple.com/documentation/uikit/uibezierpath
'iOS 응용 (swift)' 카테고리의 다른 글
Comments