Skip to content

Instantly share code, notes, and snippets.

@WorldDownTown
Last active February 24, 2020 13:19
Show Gist options
  • Save WorldDownTown/b74fbdd4120c7e88a72d3e1e9bef6fdb to your computer and use it in GitHub Desktop.
Save WorldDownTown/b74fbdd4120c7e88a72d3e1e9bef6fdb to your computer and use it in GitHub Desktop.
Loading indicator like Google's Application
import PlaygroundSupport
import UIKit
final class GoogleLoadingIndicator: UIView {
var colors: [UIColor] = []
private var index: Int = 0
private let totalDuration: TimeInterval = 2
private let movingAngle: CGFloat = 5 / 6 * .pi2
private let rotationCount: Int = 6
private let minimumAngle: CGFloat = .pi2 / 24
private let strokeAnimationKey: String = "strokeAnimationKey"
private let rotatingLayer: CALayer = .init()
private let circleLayer: CAShapeLayer = {
let layer: CAShapeLayer = .init()
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 5
return layer
}()
override func layoutSubviews() {
super.layoutSubviews()
rotatingLayer.frame = layer.bounds
circleLayer.frame = rotatingLayer.bounds
circleLayer.path = makeCirclePath()
}
func startAnimation() {
circleLayer.strokeColor = colors.first?.cgColor ?? UIColor.blue.cgColor
rotatingLayer.addSublayer(circleLayer)
layer.addSublayer(rotatingLayer)
rotatingLayer.add(makeRotationAnimation(), forKey: nil)
addStrokeAnimation()
}
func stopAnimation() {
rotatingLayer.removeFromSuperlayer()
rotatingLayer.removeAllAnimations()
circleLayer.removeAllAnimations()
}
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: -minimumAngle,
endAngle: .pi2 * 2, // 2回転
clockwise: true)
return path.cgPath
}
private func makeRotationAnimation() -> CABasicAnimation {
let animation: CABasicAnimation = .init(keyPath: "transform.rotation")
animation.duration = totalDuration
animation.fromValue = 0
animation.toValue = CGFloat.pi2
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.repeatCount = .infinity
return animation
}
private func makeStrokeAnimationGroup(startingAngle: CGFloat) -> CAAnimationGroup {
let totalAngle: CGFloat = .pi2 * 2 + minimumAngle
let animation0: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeStart))
animation0.duration = 0
animation0.beginTime = 0
animation0.fromValue = startingAngle / totalAngle
animation0.toValue = animation0.fromValue
animation0.isRemovedOnCompletion = false
animation0.fillMode = .forwards
let animation1: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation1.duration = totalDuration * 0.4
animation1.beginTime = 0
animation1.fromValue = (startingAngle + minimumAngle) / totalAngle
animation1.toValue = (startingAngle + minimumAngle + movingAngle) / totalAngle
animation1.isRemovedOnCompletion = false
animation1.fillMode = .forwards
let animation2: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeStart))
animation2.duration = totalDuration * 0.4
animation2.beginTime = totalDuration * 0.3
animation2.fromValue = startingAngle / totalAngle
animation2.toValue = (startingAngle + movingAngle) / totalAngle
animation2.isRemovedOnCompletion = false
animation2.fillMode = .forwards
let animationGroup: CAAnimationGroup = .init()
animationGroup.animations = [animation0, animation1, animation2]
animationGroup.duration = totalDuration
animationGroup.isRemovedOnCompletion = false
animationGroup.fillMode = .forwards
return animationGroup
}
private func addStrokeAnimation(startingAngle: CGFloat = 0) {
let strokeAnimation: CAAnimationGroup = makeStrokeAnimationGroup(startingAngle: startingAngle)
strokeAnimation.delegate = self
circleLayer.add(strokeAnimation, forKey: strokeAnimationKey)
}
}
// MARK: - CAAnimationDelegate
extension GoogleLoadingIndicator: CAAnimationDelegate {
func animationDidStop(_ animation: CAAnimation, finished flag: Bool) {
guard flag else { return }
if let color = colors.popLast() {
circleLayer.strokeColor = color.cgColor
colors.insert(color, at: 0)
}
index = (index + 1) % rotationCount
let startingAngle: CGFloat = (movingAngle * CGFloat(index)).truncatingRemainder(dividingBy: .pi2)
addStrokeAnimation(startingAngle: startingAngle)
}
}
private extension CGFloat {
static var pi2: Self { .pi * 2 }
}
final class MyViewController : UIViewController {
private let indicator: GoogleLoadingIndicator = .init(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
indicator.center = view.center
indicator.colors = [.blue, .green, .yellow, .red]
view.addSubview(indicator)
indicator.startAnimation()
}
}
PlaygroundPage.current.liveView = MyViewController()
@WorldDownTown
Copy link
Author

GoogleLoadingIndicator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment