Skip to content

Instantly share code, notes, and snippets.

@madcato
Created August 6, 2019 06:36
Show Gist options
  • Save madcato/19ad1609df4291c1fd1cf335ee9cc8d2 to your computer and use it in GitHub Desktop.
Save madcato/19ad1609df4291c1fd1cf335ee9cc8d2 to your computer and use it in GitHub Desktop.
Activity indicator to indicate wait with a style like the Android one.
//
// SpinnerView.swift
// veladan
//
// Created by Daniel Vela Angulo on 02/04/2019.
// Copyright © 2019 veladan. All rights reserved.
//
import UIKit
class SpinnerView: UIView {
override var layer: CAShapeLayer {
guard let layer = super.layer as? CAShapeLayer else {
fatalError("SpinnerView requires a CASHapeLayer")
}
return layer
}
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
var spinnerLineWidth: CGFloat = 3.0 {
didSet {
layer.lineWidth = spinnerLineWidth
}
}
var spinnerStrokeColor: CGColor? = UIColor.red.cgColor {
didSet {
layer.strokeColor = spinnerStrokeColor ?? UIColor.red.cgColor
}
}
var spinnerStrokeColorInitial: CGColor?
var spinnerStrokeColorEnd: CGColor?
private var duration = 0.3
var spinnerDuration: CFTimeInterval = 0.3 {
didSet {
duration = spinnerDuration
}
}
override func layoutSubviews() {
super.layoutSubviews()
layer.fillColor = nil
layer.strokeColor = spinnerStrokeColor
layer.lineWidth = spinnerLineWidth
setPath()
}
override func didMoveToWindow() {
animate()
}
private func setPath() {
layer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: layer.lineWidth / 2, dy: layer.lineWidth / 2)).cgPath
}
struct Pose {
let secondsSincePriorPose: CFTimeInterval
let start: CGFloat
let length: CGFloat
let color: CGColor?
init(_ secondsSincePriorPose: CFTimeInterval, _ start: CGFloat, _ length: CGFloat, _ color: CGColor?) {
self.secondsSincePriorPose = secondsSincePriorPose
self.start = start
self.length = length
self.color = color
}
}
private var poses: [Pose] {
return [
Pose(0.0, 0.000, 0.7, spinnerStrokeColorInitial),
Pose(duration, 0.500, 0.5, spinnerStrokeColorEnd),
Pose(duration, 1.000, 0.3, spinnerStrokeColorEnd),
Pose(duration, 1.500, 0.1, spinnerStrokeColorEnd),
Pose(duration, 1.875, 0.1, spinnerStrokeColorInitial),
Pose(duration, 2.250, 0.3, spinnerStrokeColorInitial),
Pose(duration, 2.625, 0.5, spinnerStrokeColorInitial),
Pose(duration, 3.000, 0.7, spinnerStrokeColorEnd)
]
}
func animate() {
var time: CFTimeInterval = 0
var times = [CFTimeInterval]()
var start: CGFloat = 0
var rotations = [CGFloat]()
var strokeEnds = [CGFloat]()
var colors = [CGColor]()
let tempPoses = self.poses
let totalSeconds = poses.reduce(0) { $0 + $1.secondsSincePriorPose }
for pose in tempPoses {
time += pose.secondsSincePriorPose
times.append(time / totalSeconds)
start = pose.start
rotations.append(start * 2 * .pi)
strokeEnds.append(pose.length)
if let color = pose.color {
colors.append(color)
}
}
if let last = times.last {
times.append(last)
}
rotations.append(rotations[0])
strokeEnds.append(strokeEnds[0])
animateKeyPath(keyPath: #keyPath(CAShapeLayer.strokeEnd),
duration: totalSeconds,
times: times,
values: strokeEnds)
animateKeyPath(keyPath: "transform.rotation",
duration: totalSeconds,
times: times,
values: rotations)
if !colors.isEmpty {
animateKeyPath(keyPath: #keyPath(CAShapeLayer.strokeColor),
duration: totalSeconds,
times: times,
values: colors)
}
}
func animateKeyPath(keyPath: String, duration: CFTimeInterval, times: [CFTimeInterval], values: [Any]) {
let animation = CAKeyframeAnimation(keyPath: keyPath)
animation.keyTimes = times as [NSNumber]?
animation.values = values
animation.calculationMode = CAAnimationCalculationMode.linear
animation.duration = duration
animation.repeatCount = Float.infinity
layer.add(animation, forKey: animation.keyPath)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment