Last active
February 24, 2020 13:08
-
-
Save WorldDownTown/608699c1a0d85e3dd6b1908169b00f93 to your computer and use it in GitHub Desktop.
Loading circle indicator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Author
WorldDownTown
commented
Feb 24, 2020
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment