Skip to content

Commit 7f59b89

Browse files
authored
Merge pull request #233 from mattrubin/progress-ring
Refactor progress ring to use CAShapeLayer
2 parents b33b20f + e78733e commit 7f59b89

File tree

3 files changed

+58
-56
lines changed

3 files changed

+58
-56
lines changed

Authenticator.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
C9CC09551BA91D1C008C54FE /* TableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9CC09541BA91D1C008C54FE /* TableViewModel.swift */; };
5252
C9D6C83F1906BD68004F0E08 /* SegmentedControlRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D6C83E1906BD68004F0E08 /* SegmentedControlRow.swift */; };
5353
C9D6C8461906CD54004F0E08 /* TextFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D6C8451906CD54004F0E08 /* TextFieldRow.swift */; };
54-
C9D6C84C19075044004F0E08 /* OTPProgressRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D6C84B19075044004F0E08 /* OTPProgressRing.swift */; };
54+
C9D6C84C19075044004F0E08 /* ProgressRingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D6C84B19075044004F0E08 /* ProgressRingView.swift */; };
5555
C9DE02E71ED2234D00D7E01C /* InfoList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DE02E61ED2234D00D7E01C /* InfoList.swift */; };
5656
C9DE02E91ED227D600D7E01C /* InfoListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DE02E81ED227D600D7E01C /* InfoListViewController.swift */; };
5757
C9E3FB9A1E281CBC00EFA8BB /* TokenScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E3FB991E281CBC00EFA8BB /* TokenScanner.swift */; };
@@ -172,7 +172,7 @@
172172
C9CC09541BA91D1C008C54FE /* TableViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewModel.swift; sourceTree = "<group>"; };
173173
C9D6C83E1906BD68004F0E08 /* SegmentedControlRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControlRow.swift; sourceTree = "<group>"; };
174174
C9D6C8451906CD54004F0E08 /* TextFieldRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldRow.swift; sourceTree = "<group>"; };
175-
C9D6C84B19075044004F0E08 /* OTPProgressRing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTPProgressRing.swift; sourceTree = "<group>"; };
175+
C9D6C84B19075044004F0E08 /* ProgressRingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressRingView.swift; sourceTree = "<group>"; };
176176
C9D844341D4C576B00D5E343 /* CONTRIBUTING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
177177
C9D844361D4C59D600D5E343 /* CONDUCT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONDUCT.md; sourceTree = "<group>"; };
178178
C9DE02E61ED2234D00D7E01C /* InfoList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoList.swift; sourceTree = "<group>"; };
@@ -400,7 +400,7 @@
400400
C9B7328E1C0A8AE60076F77E /* TokenListViewModel.swift */,
401401
C92708AB19CFB0750033128B /* TokenListViewController.swift */,
402402
C98C1C4517D2EDF500A07D3F /* Cells */,
403-
C9D6C84B19075044004F0E08 /* OTPProgressRing.swift */,
403+
C9D6C84B19075044004F0E08 /* ProgressRingView.swift */,
404404
CC46C4701DB3007D00EB4605 /* SearchField.swift */,
405405
);
406406
name = "Token List";
@@ -669,7 +669,7 @@
669669
C93BD6231C167CD100FFFB8F /* Root.swift in Sources */,
670670
C97CDF2C1BEEC90100D64406 /* QRScanner.swift in Sources */,
671671
C9AAB07F1B917EC3000CE547 /* TokenEditForm.swift in Sources */,
672-
C9D6C84C19075044004F0E08 /* OTPProgressRing.swift in Sources */,
672+
C9D6C84C19075044004F0E08 /* ProgressRingView.swift in Sources */,
673673
C9A1C1A91E501D8B009E65D6 /* Info.swift in Sources */,
674674
C9CC09531BA9133B008C54FE /* FocusCell.swift in Sources */,
675675
C99069D1180CBAC900BAEF53 /* TokenScannerViewController.swift in Sources */,
Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// OTPProgressRing.swift
2+
// ProgressRingView.swift
33
// Authenticator
44
//
55
// Copyright (c) 2014-2016 Authenticator authors
@@ -34,86 +34,88 @@ struct ProgressRingViewModel {
3434
}
3535
}
3636

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
4142

4243
override init(frame: CGRect) {
4344
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()
4946
}
5047

51-
// MARK: Layer
48+
required init?(coder aDecoder: NSCoder) {
49+
super.init(coder: aDecoder)
50+
configureSublayers()
51+
}
5252

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)
5557
}
5658

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+
}
5968
}
6069

6170
// MARK: Update
6271

6372
override func tintColorDidChange() {
6473
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
6680
}
6781

6882
func updateWithViewModel(_ viewModel: ProgressRingViewModel) {
69-
let path = #keyPath(ProgressLayer.progress)
83+
let path = #keyPath(RingLayer.strokeStart)
7084
let animation = CABasicAnimation(keyPath: path)
7185
let now = layer.convertTime(CACurrentMediaTime(), from: nil)
7286
animation.beginTime = now + viewModel.startTime.timeIntervalSinceNow
7387
animation.duration = viewModel.duration
7488
animation.fromValue = 0
7589
animation.toValue = 1
76-
progressLayer.add(animation, forKey: path)
90+
foregroundRingLayer.add(animation, forKey: path)
7791
}
7892
}
7993

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
8799
}
88100

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)
92103
}
93104

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()
100107

101-
override func draw(in context: CGContext) {
108+
// Inset the ring to draw within the layer's bounds.
102109
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+
}
118120
}
119121
}

Authenticator/Source/SearchField.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import UIKit
2828
// A custom view that contains a SearchTextField displaying its placeholder centered in the
2929
// text field.
3030
//
31-
// Displays a OTPProgressRing as the `leftView` control.
31+
// Displays a ProgressRingView as the `leftView` control.
3232
class SearchField: UIView {
3333

3434
override init(frame: CGRect) {
@@ -50,7 +50,7 @@ class SearchField: UIView {
5050
return textField.text
5151
}
5252

53-
let ring = OTPProgressRing(
53+
let ring = ProgressRingView(
5454
frame: CGRect(origin: .zero, size: CGSize(width: 22, height: 22))
5555
)
5656

0 commit comments

Comments
 (0)