|
1 | 1 | //
|
2 |
| -// OTPProgressRing.swift |
| 2 | +// ProgressRingView.swift |
3 | 3 | // Authenticator
|
4 | 4 | //
|
5 | 5 | // Copyright (c) 2014-2016 Authenticator authors
|
@@ -34,86 +34,88 @@ struct ProgressRingViewModel {
|
34 | 34 | }
|
35 | 35 | }
|
36 | 36 |
|
37 |
| -class OTPProgressRing: UIView { |
38 |
| - required init?(coder aDecoder: NSCoder) { |
39 |
| - super.init(coder: aDecoder) |
40 |
| - } |
| 37 | +class ProgressRingView: UIView { |
| 38 | + private let backgroundRingLayer = RingLayer() |
| 39 | + private let foregroundRingLayer = RingLayer() |
| 40 | + |
| 41 | + // MARK: Initialize |
41 | 42 |
|
42 | 43 | override init(frame: CGRect) {
|
43 | 44 | super.init(frame: frame)
|
44 |
| - self.isOpaque = false |
45 |
| - self.layer.shouldRasterize = true |
46 |
| - self.layer.rasterizationScale = UIScreen.main.scale |
47 |
| - self.layer.contentsScale = UIScreen.main.scale |
48 |
| - progressLayer.updateTintColor(tintColor) |
| 45 | + configureSublayers() |
49 | 46 | }
|
50 | 47 |
|
51 |
| - // MARK: Layer |
| 48 | + required init?(coder aDecoder: NSCoder) { |
| 49 | + super.init(coder: aDecoder) |
| 50 | + configureSublayers() |
| 51 | + } |
52 | 52 |
|
53 |
| - override public class var layerClass: AnyClass { |
54 |
| - return ProgressLayer.self |
| 53 | + private func configureSublayers() { |
| 54 | + layer.addSublayer(backgroundRingLayer) |
| 55 | + layer.addSublayer(foregroundRingLayer) |
| 56 | + updateWithRingColor(tintColor) |
55 | 57 | }
|
56 | 58 |
|
57 |
| - private var progressLayer: ProgressLayer { |
58 |
| - return layer as! ProgressLayer // swiftlint:disable:this force_cast |
| 59 | + // MARK: Layout |
| 60 | + |
| 61 | + override func layoutSublayers(of layer: CALayer) { |
| 62 | + super.layoutSublayers(of: layer) |
| 63 | + // Lay out the ring layers to fill the view's layer. |
| 64 | + if layer == self.layer { |
| 65 | + backgroundRingLayer.frame = layer.bounds |
| 66 | + foregroundRingLayer.frame = layer.bounds |
| 67 | + } |
59 | 68 | }
|
60 | 69 |
|
61 | 70 | // MARK: Update
|
62 | 71 |
|
63 | 72 | override func tintColorDidChange() {
|
64 | 73 | super.tintColorDidChange()
|
65 |
| - progressLayer.updateTintColor(tintColor) |
| 74 | + updateWithRingColor(tintColor) |
| 75 | + } |
| 76 | + |
| 77 | + private func updateWithRingColor(_ ringColor: UIColor) { |
| 78 | + foregroundRingLayer.strokeColor = ringColor.cgColor |
| 79 | + backgroundRingLayer.strokeColor = ringColor.withAlphaComponent(0.2).cgColor |
66 | 80 | }
|
67 | 81 |
|
68 | 82 | func updateWithViewModel(_ viewModel: ProgressRingViewModel) {
|
69 |
| - let path = #keyPath(ProgressLayer.progress) |
| 83 | + let path = #keyPath(RingLayer.strokeStart) |
70 | 84 | let animation = CABasicAnimation(keyPath: path)
|
71 | 85 | let now = layer.convertTime(CACurrentMediaTime(), from: nil)
|
72 | 86 | animation.beginTime = now + viewModel.startTime.timeIntervalSinceNow
|
73 | 87 | animation.duration = viewModel.duration
|
74 | 88 | animation.fromValue = 0
|
75 | 89 | animation.toValue = 1
|
76 |
| - progressLayer.add(animation, forKey: path) |
| 90 | + foregroundRingLayer.add(animation, forKey: path) |
77 | 91 | }
|
78 | 92 | }
|
79 | 93 |
|
80 |
| -private class ProgressLayer: CALayer { |
81 |
| - @NSManaged var progress: CGFloat |
82 |
| - @NSManaged var ringColor: CGColor |
83 |
| - @NSManaged var ringPartialColor: CGColor |
84 |
| - |
85 |
| - private var lineWidth: CGFloat = 1.5 { |
86 |
| - didSet { setNeedsDisplay() } |
| 94 | +private class RingLayer: CAShapeLayer { |
| 95 | + override init() { |
| 96 | + super.init() |
| 97 | + lineWidth = 1.5 |
| 98 | + fillColor = nil |
87 | 99 | }
|
88 | 100 |
|
89 |
| - fileprivate func updateTintColor(_ tintColor: UIColor) { |
90 |
| - ringColor = tintColor.cgColor |
91 |
| - ringPartialColor = tintColor.withAlphaComponent(0.2).cgColor |
| 101 | + required init?(coder aDecoder: NSCoder) { |
| 102 | + super.init(coder: aDecoder) |
92 | 103 | }
|
93 | 104 |
|
94 |
| - override class func needsDisplay(forKey key: String) -> Bool { |
95 |
| - if key == #keyPath(progress) { |
96 |
| - return true |
97 |
| - } |
98 |
| - return super.needsDisplay(forKey: key) |
99 |
| - } |
| 105 | + override func layoutSublayers() { |
| 106 | + super.layoutSublayers() |
100 | 107 |
|
101 |
| - override func draw(in context: CGContext) { |
| 108 | + // Inset the ring to draw within the layer's bounds. |
102 | 109 | let halfLineWidth = lineWidth / 2
|
103 |
| - let ringRect = self.bounds.insetBy(dx: halfLineWidth, dy: halfLineWidth) |
104 |
| - |
105 |
| - context.setLineWidth(lineWidth) |
106 |
| - |
107 |
| - context.setStrokeColor(ringPartialColor) |
108 |
| - context.strokeEllipse(in: ringRect) |
109 |
| - |
110 |
| - context.setStrokeColor(ringColor) |
111 |
| - let startAngle: CGFloat = -.pi / 2 |
112 |
| - context.addArc(center: CGPoint(x: ringRect.midX, y: ringRect.midY), |
113 |
| - radius: ringRect.width / 2, |
114 |
| - startAngle: startAngle, |
115 |
| - endAngle: 2 * .pi * CGFloat(self.progress) + startAngle, |
116 |
| - clockwise: true) |
117 |
| - context.strokePath() |
| 110 | + let ringRect = bounds.insetBy(dx: halfLineWidth, dy: halfLineWidth) |
| 111 | + |
| 112 | + // Transform the ring path to draw clockwise, starting at the top. |
| 113 | + let translationToOrigin = CGAffineTransform(translationX: -ringRect.midX, y: -ringRect.midY) |
| 114 | + let rotation = CGAffineTransform(rotationAngle: -.pi / 2) |
| 115 | + let translationFromOrigin = CGAffineTransform(translationX: ringRect.midX, y: ringRect.midY) |
| 116 | + var transform = translationToOrigin.concatenating(rotation).concatenating(translationFromOrigin) |
| 117 | + withUnsafePointer(to: &transform) { transform in |
| 118 | + path = CGPath(ellipseIn: ringRect, transform: transform) |
| 119 | + } |
118 | 120 | }
|
119 | 121 | }
|
0 commit comments