Last active
January 31, 2020 07:10
-
-
Save WorldDownTown/3b71772498c472059699f5805df34827 to your computer and use it in GitHub Desktop.
Timer Indicator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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