-
Notifications
You must be signed in to change notification settings - Fork 19
/
InstagramActivityIndicator.swift
178 lines (142 loc) · 6.25 KB
/
InstagramActivityIndicator.swift
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//
// InstagramActivityIndicator.swift
// InstagramActivityIndicator
//
// Created by John Manos on 2/3/17.
// Copyright © 2017 John Manos. All rights reserved.
//
import UIKit
import QuartzCore
@IBDesignable
public final class InstagramActivityIndicator: UIView {
/// Specifies the segment animation duration.
public var animationDuration: Double = 1
/// Specifies the indicator rotation animatino duration.
public var rotationDuration: Double = 10
/// Specifies the number of segments in the indicator.
@IBInspectable
public var numSegments: Int = 12 {
didSet {
updateSegments()
}
}
/// Specifies the stroke color of the indicator segments.
@IBInspectable
public var strokeColor : UIColor = .blue {
didSet {
segmentLayer?.strokeColor = strokeColor.cgColor
}
}
/// Specifies the line width of the indicator segments.
@IBInspectable
public var lineWidth : CGFloat = 8 {
didSet {
segmentLayer?.lineWidth = lineWidth
updateSegments()
}
}
/// A Boolean value that controls whether the receiver is hidden when the animation is stopped.
public var hidesWhenStopped: Bool = true
/// A Boolean value that returns whether the indicator is animating or not.
public private(set) var isAnimating = false
/// the layer replicating the segments.
private weak var replicatorLayer: CAReplicatorLayer!
/// the visual layer that gets replicated around the indicator.
private weak var segmentLayer: CAShapeLayer!
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
// create and add the replicator layer
let replicatorLayer = CAReplicatorLayer()
layer.addSublayer(replicatorLayer)
// configure the shape layer that gets replicated
let dot = CAShapeLayer()
dot.lineCap = kCALineCapRound
dot.strokeColor = strokeColor.cgColor
dot.lineWidth = lineWidth
dot.fillColor = nil
replicatorLayer.addSublayer(dot)
// set the weak variables after being added to the layer
self.replicatorLayer = replicatorLayer
self.segmentLayer = dot
}
override public func layoutSubviews() {
super.layoutSubviews()
// resize the replicator layer.
let maxSize = max(0,min(bounds.width, bounds.height))
replicatorLayer.bounds = CGRect(x: 0, y: 0, width: maxSize, height: maxSize)
replicatorLayer.position = CGPoint(x: bounds.width/2, y:bounds.height/2)
updateSegments()
}
/// Updates the visuals of the indicator, specifically the segment characteristics.
private func updateSegments() {
guard numSegments > 0, let segmentLayer = segmentLayer else { return }
let angle = 2*CGFloat.pi / CGFloat(numSegments)
replicatorLayer.instanceCount = numSegments
replicatorLayer.instanceTransform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0)
replicatorLayer.instanceDelay = 1.5*animationDuration/Double(numSegments)
let maxRadius = max(0,min(replicatorLayer.bounds.width, replicatorLayer.bounds.height))/2
let radius: CGFloat = maxRadius - lineWidth/2
segmentLayer.bounds = CGRect(x:0, y:0, width: 2*maxRadius, height: 2*maxRadius)
segmentLayer.position = CGPoint(x: replicatorLayer.bounds.width/2, y: replicatorLayer.bounds.height/2)
// set the path of the replicated segment layer.
let path = UIBezierPath(arcCenter: CGPoint(x: maxRadius, y: maxRadius), radius: radius, startAngle: -angle/2 - CGFloat.pi/2, endAngle: angle/2 - CGFloat.pi/2, clockwise: true)
segmentLayer.path = path.cgPath
}
/// Starts the animation of the indicator.
public func startAnimating() {
self.isHidden = false
isAnimating = true
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.byValue = CGFloat.pi*2
rotate.duration = rotationDuration
rotate.repeatCount = Float.infinity
// add animations to segment
// multiplying duration changes the time of empty or hidden segments
let shrinkStart = CABasicAnimation(keyPath: "strokeStart")
shrinkStart.fromValue = 0.0
shrinkStart.toValue = 0.5
shrinkStart.duration = animationDuration // * 1.5
shrinkStart.autoreverses = true
shrinkStart.repeatCount = Float.infinity
shrinkStart.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let shrinkEnd = CABasicAnimation(keyPath: "strokeEnd")
shrinkEnd.fromValue = 1.0
shrinkEnd.toValue = 0.5
shrinkEnd.duration = animationDuration // * 1.5
shrinkEnd.autoreverses = true
shrinkEnd.repeatCount = Float.infinity
shrinkEnd.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let fade = CABasicAnimation(keyPath: "lineWidth")
fade.fromValue = lineWidth
fade.toValue = 0.0
fade.duration = animationDuration // * 1.5
fade.autoreverses = true
fade.repeatCount = Float.infinity
fade.timingFunction = CAMediaTimingFunction(controlPoints: 0.55, 0.0, 0.45, 1.0)
replicatorLayer.add(rotate, forKey: "rotate")
segmentLayer.add(shrinkStart, forKey: "start")
segmentLayer.add(shrinkEnd, forKey: "end")
segmentLayer.add(fade, forKey: "fade")
}
/// Stops the animation of the indicator.
public func stopAnimating() {
isAnimating = false
replicatorLayer.removeAnimation(forKey: "rotate")
segmentLayer.removeAnimation(forKey: "start")
segmentLayer.removeAnimation(forKey: "end")
segmentLayer.removeAnimation(forKey: "fade")
if hidesWhenStopped {
self.isHidden = true
}
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: 180, height: 180)
}
}