Skip to content

Instantly share code, notes, and snippets.

@WorldDownTown
Last active January 31, 2020 07:10
Show Gist options
  • Save WorldDownTown/3b71772498c472059699f5805df34827 to your computer and use it in GitHub Desktop.
Save WorldDownTown/3b71772498c472059699f5805df34827 to your computer and use it in GitHub Desktop.
Timer Indicator
import UIKit
import PlaygroundSupport
import UIKit
final class TimerIndicator: UIView {
deinit {
displayLink?.invalidate()
}
weak var delegate: TimerIndicatorDelegate?
var duration: TimeInterval {
get { forwardAnimationDuration + backwardAnimationDuration }
set {
forwardAnimationDuration = newValue - backwardAnimationDuration
indicatorLayer.removeFromSuperlayer()
indicatorLayer = makeIndicatorLayer()
}
}
private var forwardAnimationDuration: TimeInterval = 2.7
private let backwardAnimationDuration: TimeInterval = 0.3
private var displayLink: CADisplayLink?
private lazy var backgroundLayer: CAShapeLayer = {
let layer: CAShapeLayer = .init()
layer.strokeColor = UIColor.lightGray.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 1
layer.path = makeCirclePath()
return layer
}()
private lazy var indicatorLayer: CAShapeLayer = makeIndicatorLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
layer.addSublayer(backgroundLayer)
}
private func makeIndicatorLayer() -> CAShapeLayer {
let layer: CAShapeLayer = .init()
layer.strokeColor = UIColor.blue.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineCap = .round
layer.lineWidth = 2
layer.speed = 0
layer.path = makeCirclePath()
layer.add(makeForwardAnimation(), forKey: "forwardAnimation")
return layer
}
private func makeBackwardIndicator() -> CAShapeLayer {
let layer: CAShapeLayer = .init()
layer.strokeColor = UIColor.blue.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineCap = .round
layer.lineWidth = 2
layer.speed = 0
layer.path = makeBackwardCirclePath()
layer.add(makeBackwardAnimation(), forKey: "backwardAnimation")
layer.frame = self.layer.bounds
return layer
}
/// 円を描くアニメーション
private func makeForwardAnimation() -> CABasicAnimation {
let animation: CABasicAnimation = .init(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = forwardAnimationDuration
return animation
}
/// 円を消すアニメーション
private func makeBackwardAnimation() -> CABasicAnimation {
let animation: CABasicAnimation = .init(keyPath: "strokeEnd")
animation.fromValue = 1
animation.toValue = 0
animation.duration = backwardAnimationDuration
return animation
}
private func makeCirclePath() -> CGPath {
let path: UIBezierPath = .init()
path.addArc(withCenter: CGPoint(x: frame.width / 2, y: frame.height / 2),
radius: frame.width / 2,
startAngle: .pi * -0.5,
endAngle: .pi * 1.5,
clockwise: true)
return path.cgPath
}
private func makeBackwardCirclePath() -> CGPath {
let path: UIBezierPath = .init()
path.addArc(withCenter: CGPoint(x: frame.width / 2, y: frame.height / 2),
radius: frame.width / 2,
startAngle: .pi * 1.5,
endAngle: .pi * -0.5,
clockwise: false)
return path.cgPath
}
func startAnimation() {
displayLink?.invalidate()
indicatorLayer.removeFromSuperlayer()
indicatorLayer = makeIndicatorLayer()
indicatorLayer.frame = layer.bounds
layer.addSublayer(indicatorLayer)
let link: CADisplayLink = .init(target: self, selector: #selector(updateDisplayLink(_:)))
link.preferredFramesPerSecond = 60
link.add(to: .current, forMode: .common)
displayLink = link
}
private func startBackwardAnimation() {
displayLink?.invalidate()
indicatorLayer.removeFromSuperlayer()
indicatorLayer = makeBackwardIndicator()
indicatorLayer.frame = layer.bounds
layer.addSublayer(indicatorLayer)
let link: CADisplayLink = .init(target: self, selector: #selector(updateBackwardDisplayLink(_:)))
link.preferredFramesPerSecond = 60
link.add(to: .current, forMode: .common)
displayLink = link
}
func stopAnimation() {
displayLink?.invalidate()
}
@objc private func updateDisplayLink(_ link: CADisplayLink) {
let delta: TimeInterval = link.targetTimestamp - link.timestamp
indicatorLayer.timeOffset = min(indicatorLayer.timeOffset + delta, forwardAnimationDuration)
if indicatorLayer.timeOffset == forwardAnimationDuration {
delegate?.timerIndicatorDidRound(self)
startBackwardAnimation()
}
}
@objc private func updateBackwardDisplayLink(_ link: CADisplayLink) {
let delta: TimeInterval = link.targetTimestamp - link.timestamp
indicatorLayer.timeOffset = min(indicatorLayer.timeOffset + delta, backwardAnimationDuration)
if indicatorLayer.timeOffset == backwardAnimationDuration {
startAnimation()
}
}
}
protocol TimerIndicatorDelegate: AnyObject {
func timerIndicatorDidRound(_ indicator: TimerIndicator)
}
final class MyViewController: UIViewController {
deinit {
indicator.invalidate()
}
private let indicator: TimerIndicator = .init(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
override func loadView() {
super.loadView()
indicator.center = view.center
indicator.duration = 5
let view: UIView = .init()
view.backgroundColor = .white
view.addSubview(indicator)
indicator.delegate = self
self.view = view
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
indicator.startAnimation()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
indicator.stopAnimation()
}
}
// MARK: - TimerIndicatorDelegate
extension MyViewController: TimerIndicatorDelegate {
func timerIndicatorDidRound(_ indicator: TimerIndicator) {
print("rounded")
}
}
PlaygroundPage.current.liveView = MyViewController()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment