Skip to content

Instantly share code, notes, and snippets.

@WorldDownTown
Last active February 24, 2020 13:08
Show Gist options
  • Save WorldDownTown/608699c1a0d85e3dd6b1908169b00f93 to your computer and use it in GitHub Desktop.
Save WorldDownTown/608699c1a0d85e3dd6b1908169b00f93 to your computer and use it in GitHub Desktop.
Loading circle indicator
import PlaygroundSupport
import UIKit
final class LoadingIndicator: UIView {
var color: UIColor = .blue
private var index: Int = 0
private let lap: TimeInterval = 2
private let movingAngle: CGFloat = 5 / 6 * .pi2
private let rotationCount: Int = 6
private let minimumAngle: CGFloat = .pi2 / 24
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 = color.cgColor
rotatingLayer.addSublayer(circleLayer)
layer.addSublayer(rotatingLayer)
rotatingLayer.add(makeRotationAnimation(), forKey: nil)
circleLayer.add(makeStrokeAnimationGroup(), forKey: nil)
}
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 = lap
animation.fromValue = 0
animation.toValue = CGFloat.pi2
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.repeatCount = .infinity
return animation
}
private func makeStrokeAnimationGroup() -> CAAnimationGroup {
let animationGroup: CAAnimationGroup = .init()
animationGroup.duration = lap * TimeInterval(rotationCount)
animationGroup.animations = (0..<rotationCount).flatMap(lapAnimations)
animationGroup.isRemovedOnCompletion = false
animationGroup.fillMode = .forwards
animationGroup.repeatCount = .infinity
return animationGroup
}
private func lapAnimations(index: Int) -> [CABasicAnimation] {
let totalAngle: CGFloat = .pi2 * 2 + minimumAngle
let delay: TimeInterval = lap * TimeInterval(index)
let startingAngle: CGFloat = (movingAngle * CGFloat(index)).truncatingRemainder(dividingBy: .pi2)
let animation0: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeStart))
animation0.duration = 0
animation0.beginTime = delay
animation0.fromValue = startingAngle / totalAngle
animation0.toValue = animation0.fromValue
animation0.isRemovedOnCompletion = false
animation0.fillMode = .forwards
let animation1: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation1.duration = lap * 0.4
animation1.beginTime = delay
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 = lap * 0.4
animation2.beginTime = delay + lap * 0.3
animation2.fromValue = startingAngle / totalAngle
animation2.toValue = (startingAngle + movingAngle) / totalAngle
animation2.isRemovedOnCompletion = false
animation2.fillMode = .forwards
return [animation0, animation1, animation2]
}
}
private extension CGFloat {
static var pi2: Self { .pi * 2 }
}
final class MyViewController : UIViewController {
private let indicator: LoadingIndicator = .init(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
indicator.center = view.center
indicator.color = .red
view.addSubview(indicator)
indicator.startAnimation()
}
}
PlaygroundPage.current.liveView = MyViewController()
@WorldDownTown
Copy link
Author

LoadingIndicator

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