Custom indicator with rotate blink animation like UIActivityIndicatorView - ios

I am trying to make custom activity indicator, see the indicator class below
import UIKit
class MyIndicator: UIView {
let gap = CGFloat(.pi/4 / 6.0)
var count = 0
override func draw(_ rect: CGRect) {
super.draw(rect)
}
func blink() {
backgroundColor = .clear
let duration: CFTimeInterval = 1.2
//let beginTime = CACurrentMediaTime()
let beginTimes: [CFTimeInterval] = [0.25, 1, 1.75, 2.5]
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Animation
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.keyTimes = [0, 0.5, 1]
animation.timingFunctions = [timingFunction, timingFunction]
animation.values = [1, 0.3, 1]
animation.duration = duration
animation.repeatCount = HUGE
animation.isRemovedOnCompletion = false
for i in 0...3 {
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.fillColor = UIColor.clear.cgColor
shape.lineWidth = 6.8
shape.strokeColor = UIColor.blue.cgColor
let startAngle:CGFloat = CGFloat(i) * CGFloat(Double.pi/2) + gap
let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap * 2
shape.path = UIBezierPath(arcCenter: center, radius: -20, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
animation.beginTime = beginTimes[i]
shape.add(animation, forKey: "animation")
self.layer.addSublayer(shape)
}
}
func startAnimating() {
blink()
}
}
let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(indicator)
indicator.startAnimating()
I have attached my current result.
But you can see that the animation is not in circular motion like standard UIActivityIndicatorView. Can anyone help me to fix this.

Try using a CAReplicatorLayer and instance delay to get everything in sync. Here is a Playground. I am not 100% sure on the visual you want but this should be close.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyIndicator: UIView {
let gap = CGFloat(.pi/4 / 6.0)
private var replicatorLayer = CAReplicatorLayer()
private var mainShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
func commonSetup(){
mainShapeLayer = CAShapeLayer()
mainShapeLayer.frame = self.bounds
mainShapeLayer.fillColor = UIColor.clear.cgColor
mainShapeLayer.lineWidth = 6.8
mainShapeLayer.strokeColor = UIColor.blue.cgColor
let startAngle:CGFloat = CGFloat(Double.pi * 2) + gap/2
let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap/2
mainShapeLayer.path = UIBezierPath(arcCenter: center, radius: self.bounds.midX - 10, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = self.bounds
replicatorLayer.instanceCount = 4
let angle = (Double.pi * 2)/4
replicatorLayer.instanceTransform = CATransform3DRotate(CATransform3DIdentity, CGFloat(angle), 0, 0, 1)
replicatorLayer.addSublayer(mainShapeLayer)
replicatorLayer.opacity = 0
self.layer.addSublayer(replicatorLayer)
}
func animate(){
let defaultDuration : Double = 0.75
let animate = CAKeyframeAnimation(keyPath: "opacity")
animate.values = [1, 0.3, 1]
animate.keyTimes = [0, 0.5, 1]
animate.repeatCount = .greatestFiniteMagnitude
animate.duration = defaultDuration
animate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
replicatorLayer.instanceDelay = defaultDuration/4
self.mainShapeLayer.add(animate, forKey: nil)
let opacityIn = CABasicAnimation(keyPath: "opacity")
opacityIn.fromValue = 1
opacityIn.toValue = 0
opacityIn.duration = 0.2
replicatorLayer.add(opacityIn, forKey: nil)
self.replicatorLayer.opacity = 1
}
func stopAnimating(){
CATransaction.begin()
let opacityOut = CABasicAnimation(keyPath: "opacity")
opacityOut.fromValue = 1
opacityOut.toValue = 0
opacityOut.duration = 0.2
CATransaction.setCompletionBlock {
[weak self] in
self?.mainShapeLayer.removeAllAnimations()
}
replicatorLayer.add(opacityOut, forKey: nil)
self.replicatorLayer.opacity = 0
CATransaction.commit()
}
override func layoutSubviews() {
super.layoutSubviews()
mainShapeLayer.frame = self.bounds
replicatorLayer.frame = self.bounds
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
indicator.animate()
//just to simulate starting and stoping
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10) {
indicator.stopAnimating()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
indicator.animate()
}
}
view.addSubview(indicator)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Related

why isn't the CAShapeLayer showing up sometimes?

I am using the following custom UIView class as a loading indicator in my app. Most of the time it works great and I can just uses loadingView.startLoading() to make it appear. However, in rare cases, only the UIView appears with a shadow behind it, and the CAShapeLayer is nowhere to be seen. What could be preventing the CAShapeLayer from appearing on top of the LoadingView UIView?
LoadingView
class LoadingView: UIView {
//MARK: - Properties and Init
let circleLayer = CAShapeLayer()
private var circlePath : UIBezierPath = .init()
let size : CGFloat = 80
init() {
super.init(frame: CGRect(x: 0, y: 0, width: size, height: size))
tag = 100
addShadow(shadowColor: UIColor.label.cgColor, shadowOffset: CGSize(width: 0, height: 0), shadowOpacity: 0.3, shadowRadius: 3)
backgroundColor = .secondarySystemBackground
self.layer.addSublayer(circleLayer)
calculateCirclePath()
animateCircle(duration: 1, repeats: true)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Private UI Setup Methods
private func calculateCirclePath() {
self.circlePath = UIBezierPath(arcCenter: CGPoint(x: size / 2, y: size / 2), radius: size / 3, startAngle: 0.0, endAngle: CGFloat(Double.pi*2), clockwise: true)
}
override func layoutSubviews() {
circleLayer.frame = self.layer.bounds
}
private func animateCircle(duration: TimeInterval, repeats: Bool) {
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.blue.cgColor
circleLayer.lineWidth = 5.0
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
self.layer.addSublayer(circleLayer)
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.repeatCount = repeats ? .infinity : 1
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
circleLayer.strokeEnd = 0
circleLayer.add(animation, forKey: "animateCircle")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.repeatCount = repeats ? .infinity : 1
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = Double.pi*3
rotationAnimation.duration = duration
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(rotationAnimation, forKey: nil)
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.repeatCount = repeats ? .infinity : 1
fadeAnimation.fromValue = 1
fadeAnimation.toValue = 0
fadeAnimation.duration = duration
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(fadeAnimation, forKey: nil)
}
}
Extension for ViewController
extension ViewController {
func startLoading() {
view.addSubview(loadingView)
loadingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.widthAnchor.constraint(equalToConstant: CGFloat(80)),
loadingView.heightAnchor.constraint(equalToConstant: CGFloat(80)),
])
loadingView.isHidden = false
}
func stopLoading() {
loadingView.isHidden = true
}
}
Because a sublayer and animation can't happen in one go, I changed the code to what is shown below and now it works perfectly!
LoadingView
class LoadingView: UIView {
//MARK: - Properties and Init
let circleLayer = CAShapeLayer()
private var circlePath : UIBezierPath = .init()
let size : CGFloat = 80
init() {
super.init(frame: CGRect(x: 0, y: 0, width: size, height: size))
tag = 100
addShadow(shadowColor: UIColor.label.cgColor, shadowOffset: CGSize(width: 0, height: 0), shadowOpacity: 0.3, shadowRadius: 3)
backgroundColor = .secondarySystemBackground
layer.cornerRadius = size/8
self.layer.addSublayer(circleLayer)
calculateCirclePath()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.blue.cgColor
circleLayer.lineWidth = 5.0
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
self.layer.addSublayer(circleLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Private UI Setup Methods
private func calculateCirclePath() {
self.circlePath = UIBezierPath(arcCenter: CGPoint(x: size / 2, y: size / 2), radius: size / 3, startAngle: 0.0, endAngle: CGFloat(Double.pi*2), clockwise: true)
}
override func layoutSubviews() {
circleLayer.frame = self.layer.bounds
}
func animateCircle(duration: TimeInterval, repeats: Bool) {
// Setup the CAShapeLayer with the path, colors, and line width
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.repeatCount = repeats ? .infinity : 1
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
circleLayer.strokeEnd = 0
circleLayer.add(animation, forKey: "animateCircle")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.repeatCount = repeats ? .infinity : 1
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = Double.pi*3
rotationAnimation.duration = duration
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(rotationAnimation, forKey: nil)
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.repeatCount = repeats ? .infinity : 1
fadeAnimation.fromValue = 1
fadeAnimation.toValue = 0
fadeAnimation.duration = duration
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(fadeAnimation, forKey: nil)
}
}
Extension for ViewController
extension ViewController {
func startLoading() {
view.addSubview(loadingView)
loadingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.widthAnchor.constraint(equalToConstant: CGFloat(80)),
loadingView.heightAnchor.constraint(equalToConstant: CGFloat(80)),
])
loadingView.isHidden = false
loadingView.animateCircle(duration: 1, repeats: true)
}
func stopLoading() {
loadingView.isHidden = true
}
}
its better practice to call this method in main thread using dispatchQueue as sometime control may be in background thread so it must get returned to main thread before performing any UI updates

CAShapeLayer and CABasicAnimation for circle - loader style - blinking

I'm having fun with CAShapeLayer on a playground project.
I want to create an animation coloring the stroke of a CALayer in a clock way, then, an animation removing the stroke color, in the same direction. So it gives the effect to putting and removing the strokes.
So far, it looks pretty much like what in wanted except, just before calling fullAnimate after the second time, the stroke blink like it was "full" ,then it disappears, then the animation plays again.
I agree my code ain't the best so far since I'm just playing with it yet, but I've look for explanation and did not found any usefull answer.
Can someone explain to me what's happening there ? and how to avoid it ?
here is my playground file
import UIKit
import PlaygroundSupport
enum CircleProgressionViewAnimationState {
case start, firstAnimation, secondAnimation, progress, stop
}
class CircleProgressionView : UIView {
static let offset: CGFloat = 10.0
private var path : UIBezierPath? {
didSet {
circleLayer.path = path?.cgPath
}
}
private var state : CircleProgressionViewAnimationState {
didSet {
observe(change: state)
}
}
private var progressionPath : UIBezierPath?
var circleLayerContainer = CALayer()
var circleLayer : CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 4
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 0.6666666865, green: 0.6666666865, blue: 0.6666666865, alpha: 1)
return shapeLayer
}()
var circleProgressLayer : CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 15
shapeLayer.cornerRadius = 2
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1)
shapeLayer.lineCap = CAShapeLayerLineCap.round
return shapeLayer
}()
override init(frame: CGRect) {
state = .stop
super.init(frame:frame)
initLayerValues()
}
required init?(coder aDecoder: NSCoder) {
state = .stop
super.init(coder:aDecoder)
initLayerValues()
}
private func initLayerValues() {
let side = min(frame.width, frame.height)
circleLayerContainer.frame = CGRect(x: 0, y: 0, width: side, height: side)
let offset = CircleProgressionView.offset
let bezierSide = side - (offset * 2)
let bezierRect = CGRect(x:offset,
y:offset,
width: bezierSide,
height:bezierSide)
path = UIBezierPath(roundedRect: bezierRect,
cornerRadius: CGFloat(bezierSide / 2))
circleLayerContainer.addSublayer(circleLayer)
layer.addSublayer(circleLayerContainer)
layer.addSublayer(circleProgressLayer)
}
func setProgressionPath(_ progressionInPercent: CGFloat) {
let progression = progressionInPercent / 100 * (360)
let rad = (progression + 270) * CGFloat.pi / 180
let start = 270 * CGFloat.pi / 180
let offset = CircleProgressionView.offset
progressionPath = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2),
radius: (self.frame.size.height - offset - circleProgressLayer.lineWidth / 2) / 2,
startAngle: start,
endAngle: rad,
clockwise: true)
}
func observe(change: CircleProgressionViewAnimationState) {
print(change)
switch change {
case .firstAnimation:
fullAnimate { self.state = .secondAnimation }
break
case .secondAnimation:
emptyAnimate { self.state = .firstAnimation }
break
case .start, .progress, .stop: break
}
}
func animate(loop: Bool) {
var completion : ()->Void = {}
if loop {
state = .start
completion = { self.state = .secondAnimation }
}
fullAnimate(completion: completion)
}
func fullAnimate(completion: #escaping ()->Void) {
self.state = .progress
circleProgressLayer.removeAllAnimations()
circleProgressLayer.path
CATransaction.begin()
circleProgressLayer.path = progressionPath?.cgPath
CATransaction.setCompletionBlock{ completion() }
let animation : CABasicAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name:
CAMediaTimingFunctionName.easeInEaseOut)
circleProgressLayer.add(animation, forKey: #keyPath(CAShapeLayer.strokeEnd))
CATransaction.commit()
}
func emptyAnimate(completion: #escaping ()->Void) {
self.state = .progress
circleProgressLayer.removeAllAnimations()
CATransaction.begin()
circleProgressLayer.path = progressionPath?.reversing().cgPath
CATransaction.setCompletionBlock{ completion() }
let animation : CABasicAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation.fromValue = 1.0
animation.toValue = 0.0
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name:
CAMediaTimingFunctionName.easeInEaseOut)
circleProgressLayer.add(animation, forKey: #keyPath(CAShapeLayer.strokeEnd))
CATransaction.commit()
}
}
var container : UIView = {
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
let view = UIView(frame: frame)
view.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
return view
}()
let circle = CircleProgressionView(frame: container.frame)
PlaygroundPage.current.liveView = container
circle.setProgressionPath(100)
container.addSubview(circle)
circle.animate(loop: true)
In the empty animation block add the following two lines and you can figure out the reason easily.
func emptyAnimate(completion: #escaping ()->Void) {
self.state = .progress
circleProgressLayer.removeAllAnimations()
CATransaction.begin()
. .....
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
......
}

How do i call functions of a class - Swift

Im still trying to learn this portion of things. I've looked around and read a few questions about it but truthfully I dont understand any of it.
I've got a circle class that creates and draws a circle
class CircleView: UIView {
var circleLayer: CAShapeLayer!
var isAnimating = false
override init(frame: CGRect) {
let fColor = UIColor.clear.cgColor
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1).cgColor
circleLayer.lineWidth = 9.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setCircleClockwise(){
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
self.circleLayer.removeFromSuperlayer()
self.circleLayer = formatCirle(circlePath: circlePath)
self.layer.addSublayer(self.circleLayer)
}
func setCircleCounterClockwise(){
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: false)
self.circleLayer.removeFromSuperlayer()
self.circleLayer = formatCirle(circlePath: circlePath)
self.layer.addSublayer(self.circleLayer)
}
func formatCirle(circlePath: UIBezierPath) -> CAShapeLayer{
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.clear.cgColor
circleShape.strokeColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1).cgColor
circleShape.lineWidth = 9.0;
circleShape.strokeEnd = 0.0
return circleShape
}
func animate(duration: TimeInterval){
self.isAnimating = true
self.animateCircleFull(duration: 1)
}
func endAnimate(){
self.isAnimating = false
}
func animateCircleFull(duration: TimeInterval) {
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 1.0
CATransaction.setCompletionBlock {
self.setCircleCounterClockwise()
self.animateCircleEmpty(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
func animateCircleEmpty(duration: TimeInterval){
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 1
animation.toValue = 0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 0
CATransaction.setCompletionBlock {
self.setCircleClockwise()
self.animateCircleFull(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
Which is being called from my viewController with the below function. It all works fine but what i cant work out is how do i go about calling the endAnimation function on the same circle?
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(100)
var circleHeight = circleWidth
var bgColor: UIColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1)
// Create a new CircleView
let circleView = CircleView(frame: CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: circleWidth, height: circleHeight))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
cv = circleView
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animate(duration: 1)
let imageName = "ButtonBackground.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: 100, height: 100)
view.addSubview(imageView)
view.bringSubview(toFront: circleView)
}
For calling endAnimation function on the same circle you have to declare property in your ViewController class.
import UIKit
class ViewController: UIViewController {
var circleView = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
self.addCircleView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(100)
var circleHeight = circleWidth
var bgColor: UIColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1)
// Create a new CircleView
self.circleView = CircleView(frame: CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: circleWidth, height: circleHeight))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
cv = circleView
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animate(duration: 1)
let imageName = "ButtonBackground.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: 100, height: 100)
view.addSubview(imageView)
view.bringSubview(toFront: circleView)
//now you are able to call endAnimation function
self.circleView.endAnimate()
}
}

CAShapeLayer is not touchable?

I created a CAShapeLayer and added it as a subLayer.
Then I overridden the:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
And it get called successfully, but after I move this layer using CABasicAnimation.
It's not longer touchable.
The strange thing is that the original empty place of the CAShapeLayer is touchable after the animation finishes.
The structure of the view is as follows:
I subclass UIView and add to it the layer, let's name it view X.
Then I take view X and add it to anther view.
Then I animate view X using CABasicAnimation
Code:
class AIFloatingMenuItem: UIView
{
var image = UIImage()
var color = UIColor.brown
var location:CGPoint?
var fontName:String?
var text:String?
let rectShape = CAShapeLayer()
convenience init(title:String, img:UIImage, bgColor:UIColor)
{
self.init(frame: CGRect(x: 0, y: 0, width: ITEM_SIZE, height: ITEM_SIZE))
self.image = img
self.color = bgColor
self.drawCircle()
backgroundColor = UIColor.red
}
override init(frame: CGRect)
{
super.init(frame: frame)
}
//MARK: - Drawing -
func drawCircle()
{
rectShape.anchorPoint = CGPoint(x:0.5 , y:0.5)
rectShape.fillColor = color.cgColor
let path = UIBezierPath(arcCenter: CGPoint(x: ITEM_SIZE/2, y: ITEM_SIZE/2), radius: ITEM_SIZE/2, startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
rectShape.path = path.cgPath
layer.addSublayer(rectShape)
}
}
class AIFloatingMenu: UIView, CAAnimationDelegate
{
var item:AIFloatingMenuItem?
override func awakeFromNib()
{
super.awakeFromNib()
animateItem1()
}
func animateItem1()
{
item = AIFloatingMenuItem(title: "title", img: UIImage(named : "icon-plus")!, bgColor: .green)
let itemsInitialPostition = CGPoint(x: 250, y: 300)
item?.layer.position = itemsInitialPostition
self.addSubview(item!)
drawAnimateY(end: CGPoint(x: item!.layer.position.x, y: 300), isEnd: false, item: item!, delay: 0.5)
}
func drawAnimateY(end:CGPoint, isEnd:Bool, item:AIFloatingMenuItem, delay:Double)
{
let pathAnimation = CASpringAnimation(keyPath: "position")
pathAnimation.damping = 13
CATransaction.begin()
CATransaction.setCompletionBlock
{
}
pathAnimation.delegate = self
pathAnimation.duration = 1.5
pathAnimation.fromValue = item.layer.position
pathAnimation.toValue = end
pathAnimation.fillMode = kCAFillModeForwards
pathAnimation.isRemovedOnCompletion = false
pathAnimation.beginTime = CACurrentMediaTime() + delay
item.layer.add(pathAnimation, forKey: nil)
CATransaction.commit()
}
}
I add a view in the storyboard and set it's class to AIFloatingMenu.
I have been able to solve my issue by changing the position of the item being animated to the destination location after the item finishes the Animation:
func drawAnimateY(end:CGPoint, isEnd:Bool, item:AIFloatingMenuItem, delay:Double)
{
let pathAnimation = CASpringAnimation(keyPath: "position")
pathAnimation.damping = 13
CATransaction.begin()
CATransaction.setCompletionBlock
{
item.layer.position = end// changing the position of item to the destination location
}
pathAnimation.delegate = self
pathAnimation.duration = 1.5
pathAnimation.fromValue = item.layer.position
pathAnimation.toValue = end
pathAnimation.fillMode = kCAFillModeForwards
pathAnimation.isRemovedOnCompletion = false
pathAnimation.beginTime = CACurrentMediaTime() + delay
item.layer.add(pathAnimation, forKey: nil)
CATransaction.commit()
}

Create spinning circle loading animation

I'm trying to create a spinning circle loader animation like in the following Android project (https://github.com/pedant/sweet-alert-dialog).
I don't need the entire popup dialog - just the spinning part. It changes colors and spins indefinitly (until I choose to dismiss it).
I'm kind of new to swift and I've never been the kind to do animations. Here's what I have so far (found code in similar project for iOS):
The layers setup:
outlineLayer.position = CGPointMake(0,
0);
outlineLayer.path = outlineCircle
outlineLayer.fillColor = UIColor.clearColor().CGColor;
outlineLayer.strokeColor = UIColor(red: 150.0/255.0, green: 216.0/255.0, blue: 115.0/255.0, alpha: 1.0).CGColor;
outlineLayer.lineCap = kCALineCapRound
outlineLayer.lineWidth = 4;
outlineLayer.opacity = 0.1
self.layer.addSublayer(outlineLayer)
circleLayer.position = CGPointMake(0,
0);
circleLayer.path = path
circleLayer.fillColor = UIColor.clearColor().CGColor;
circleLayer.strokeColor = UIColor(red: 150.0/255.0, green: 216.0/255.0, blue: 115.0/255.0, alpha: 1.0).CGColor;
circleLayer.lineCap = kCALineCapRound
circleLayer.lineWidth = 4;
circleLayer.actions = [
"strokeStart": NSNull(),
"strokeEnd": NSNull(),
"transform": NSNull()
]
self.layer.addSublayer(circleLayer)
Animation:
let strokeStart = CABasicAnimation(keyPath: "strokeStart")
let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
let factor = 0.545
let timing = CAMediaTimingFunction(controlPoints: 0.3, 0.6, 0.8, 1.2)
strokeEnd.fromValue = 0.00
strokeEnd.toValue = 0.93
strokeEnd.duration = 10.0 * factor
strokeEnd.timingFunction = timing
strokeEnd.autoreverses = true
strokeStart.fromValue = 0.0
strokeStart.toValue = 0.68
strokeStart.duration = 10.0 * factor
strokeStart.beginTime = CACurrentMediaTime() + 3.0 * factor
strokeStart.fillMode = kCAFillModeBackwards
strokeStart.timingFunction = timing
strokeStart.repeatCount = HUGE
circleLayer.strokeStart = 0.68
circleLayer.strokeEnd = 0.93
self.circleLayer.addAnimation(strokeEnd, forKey: "strokeEnd")
self.circleLayer.addAnimation(strokeStart, forKey: "strokeStart")
but what I have is not nearly close and I have no idea where to go from here. What I'm doing is changing a value and running seeing how it affects but I feel like I'm lost here.
How can I achieve such animation like in the example?
I didn't closely analyze the exact parameters of the animation, but this looks good to me:
import UIKit
#IBDesignable
class SpinnerView : UIView {
override var layer: CAShapeLayer {
get {
return super.layer as! CAShapeLayer
}
}
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
layer.fillColor = nil
layer.strokeColor = UIColor.black.cgColor
layer.lineWidth = 3
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
init(_ secondsSincePriorPose: CFTimeInterval, _ start: CGFloat, _ length: CGFloat) {
self.secondsSincePriorPose = secondsSincePriorPose
self.start = start
self.length = length
}
}
class var poses: [Pose] {
get {
return [
Pose(0.0, 0.000, 0.7),
Pose(0.6, 0.500, 0.5),
Pose(0.6, 1.000, 0.3),
Pose(0.6, 1.500, 0.1),
Pose(0.2, 1.875, 0.1),
Pose(0.2, 2.250, 0.3),
Pose(0.2, 2.625, 0.5),
Pose(0.2, 3.000, 0.7),
]
}
}
func animate() {
var time: CFTimeInterval = 0
var times = [CFTimeInterval]()
var start: CGFloat = 0
var rotations = [CGFloat]()
var strokeEnds = [CGFloat]()
let poses = type(of: self).poses
let totalSeconds = poses.reduce(0) { $0 + $1.secondsSincePriorPose }
for pose in poses {
time += pose.secondsSincePriorPose
times.append(time / totalSeconds)
start = pose.start
rotations.append(start * 2 * .pi)
strokeEnds.append(pose.length)
}
times.append(times.last!)
rotations.append(rotations[0])
strokeEnds.append(strokeEnds[0])
animateKeyPath(keyPath: "strokeEnd", duration: totalSeconds, times: times, values: strokeEnds)
animateKeyPath(keyPath: "transform.rotation", duration: totalSeconds, times: times, values: rotations)
animateStrokeHueWithDuration(duration: totalSeconds * 5)
}
func animateKeyPath(keyPath: String, duration: CFTimeInterval, times: [CFTimeInterval], values: [CGFloat]) {
let animation = CAKeyframeAnimation(keyPath: keyPath)
animation.keyTimes = times as [NSNumber]?
animation.values = values
animation.calculationMode = .linear
animation.duration = duration
animation.repeatCount = Float.infinity
layer.add(animation, forKey: animation.keyPath)
}
func animateStrokeHueWithDuration(duration: CFTimeInterval) {
let count = 36
let animation = CAKeyframeAnimation(keyPath: "strokeColor")
animation.keyTimes = (0 ... count).map { NSNumber(value: CFTimeInterval($0) / CFTimeInterval(count)) }
animation.values = (0 ... count).map {
UIColor(hue: CGFloat($0) / CGFloat(count), saturation: 1, brightness: 1, alpha: 1).cgColor
}
animation.duration = duration
animation.calculationMode = .linear
animation.repeatCount = Float.infinity
layer.add(animation, forKey: animation.keyPath)
}
}
Try out my three custom Loader screen very simple :
Write below code in Viewcontoller.swift file
class ViewController: UIViewController {
var signView = SignView(frame: CGRect.zero)
var testView = TestView(frame: CGRect.zero)
var testView1 = TestView1(frame: CGRect.zero)
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.orange
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addSignView()
//addTestView()
//addTestView1()
}
func addSignView() {
signView.frame = CGRect(x: 0,
y: 0,
width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height)
self.view.addSubview(signView)
signView.addAnimationLayer()
}
func addTestView() {
let boxSize: CGFloat = 200.0
testView.frame = CGRect(x: 16,
y: 350,
width: boxSize,
height: boxSize)
self.view.addSubview(testView)
testView.addAnimationLayer()
}
func addTestView1() {
testView1.frame = CGRect(x: 0,
y: 0,
width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height)
self.view.addSubview(testView1)
testView1.addAnimationLayer()
}}
Now Add 3 Files inherit with UiView named > SignView , TestView and TestView1
Code for SignView.swift file
class SignView: UIView {
let upCircleLayer = CAShapeLayer.init()
var path = UIBezierPath.init()
var animationDuration : Double = 2
var frameHeight : CGFloat = 50.0
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.black.withAlphaComponent(0.5)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var signWavePath : UIBezierPath {
var clockCycle = true
let yPoint = self.frame.size.height/2
frameHeight = self.frame.size.width/6
for x in 1...24{
if x%2 != 0 {
let xpath = UIBezierPath(arcCenter: CGPoint(x: CGFloat(x)*frameHeight/2, y: yPoint),
radius: frameHeight/2,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: clockCycle)
path.append(xpath)
if(clockCycle){
clockCycle = false
}
else{
clockCycle = true
}
}
}
return path;
}
func addAnimationLayer() {
// Add Upper Circle Layer
upCircleLayer.fillColor = UIColor.clear.cgColor
upCircleLayer.strokeColor = UIColor.white.cgColor
upCircleLayer.lineWidth = 8.0
upCircleLayer.path = signWavePath.cgPath
layer.addSublayer(upCircleLayer)
animateStrokeUpCircle()
Timer.scheduledTimer(timeInterval: animationDuration, target: self, selector: #selector(animateStrokeUpCircle), userInfo: nil, repeats: true)
}
func animateStrokeUpCircle() {
let strokeAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0.0
strokeAnimation.toValue = 1.0
strokeAnimation.duration = animationDuration
strokeAnimation.isRemovedOnCompletion = false
upCircleLayer.add(strokeAnimation, forKey: nil)
expand1()
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,sin(self.frame.width)]
expandAnimation.toValue = [-self.frame.width,cos(self.frame.width)]
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
}
}
Code for TestView File :
class TestView: UIView {
let upCircleLayer = CAShapeLayer.init()
let downCircleLayer = CAShapeLayer.init()
var path1 = UIBezierPath.init()
var path2 = UIBezierPath.init()
var animationDirection : Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var up1Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
}
var down2Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
}
var up22Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
}
var down11Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
}
var up2Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 0.0,
endAngle: 180.0 * .pi/180.0,
clockwise: true)
}
var down1Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 0.0,
endAngle: 180.0 * .pi/180.0,
clockwise: false)
}
func addAnimationLayer() {
path1.append(up1Circle);
path1.append(down2Circle);
path2.append(down11Circle)
path2.append(up22Circle)
// Add Upper Circle Layer
upCircleLayer.fillColor = UIColor.clear.cgColor
upCircleLayer.strokeColor = UIColor.black.cgColor
upCircleLayer.lineWidth = 8.0
upCircleLayer.path = path1.cgPath
layer.addSublayer(upCircleLayer)
Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(expand1), userInfo: nil, repeats: true)
}
func expand() {
if animationDirection{
//upCircleLayer.path = path1.cgPath
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path1.cgPath
expandAnimation.toValue = path2.cgPath
expandAnimation.duration = 1.5
//expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
animationDirection = false
}
else{
//upCircleLayer.path = path2.cgPath
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path2.cgPath
expandAnimation.toValue = path1.cgPath
expandAnimation.duration = 1.5
//expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
animationDirection = true
}
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,self.frame.height/2]
expandAnimation.toValue = 500
expandAnimation.duration = 2.0
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
}
}
And Code for TestView1.swift file
class TestView1: UIView {
let animationLayer = CAShapeLayer.init()
var path1 = UIBezierPath.init()
var path2 = UIBezierPath.init()
var path = UIBezierPath.init()
var circleRadius : CGFloat = 26.0;
var centerLineHeight : CGFloat = 40.0
var animationDuration : Double = 2.0
var animationDirection : Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.black
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var centerMainLine: UIBezierPath {
let frameSize = self.frame.size
let centerLine = UIBezierPath()
centerLine.move(to: CGPoint(x: frameSize.width/2, y: frameSize.height/2 - centerLineHeight/2))
centerLine.addLine(to: CGPoint(x: frameSize.width/2, y: frameSize.height/2 + centerLineHeight/2))
return centerLine
}
var upLeftCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 - circleRadius, y: frameSize.height/2 - centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
var upRightCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 + circleRadius, y: frameSize.height/2 - centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
var downLeftCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 - circleRadius, y: frameSize.height/2 + centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
var downRightCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 + circleRadius, y: frameSize.height/2 + centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
func drawUpCircle(centerPoint:CGPoint, radiusValue:CGFloat) -> UIBezierPath {
let halfCircle = UIBezierPath(arcCenter: centerPoint,
radius: radiusValue,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
func drawDownCircle(centerPoint:CGPoint,radiusValue:CGFloat) -> UIBezierPath {
let halfCircle = UIBezierPath(arcCenter: centerPoint,
radius: radiusValue,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
func drawLine(fromPoint:CGPoint,toPoint:CGPoint) -> UIBezierPath {
let line = UIBezierPath()
line.move(to: fromPoint)
line.addLine(to: toPoint)
return line
}
func addAnimationLayer() {
createPathOne()
createPathTwo()
createPath()
// set Animation Layer design
animationLayer.fillColor = UIColor.clear.cgColor
animationLayer.strokeColor = UIColor.white.cgColor
animationLayer.lineWidth = 8.0
animationLayer.path = path.cgPath
layer.addSublayer(animationLayer)
expand1()
Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(expand1), userInfo: nil, repeats: true)
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,0]
expandAnimation.toValue = [-2000,0]
expandAnimation.duration = 10.0
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
}
func expand() {
animationLayer.path = centerMainLine.cgPath
if animationDirection{
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path1.cgPath
expandAnimation.toValue = path2.cgPath
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeBackwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
animationDirection = false
}
else{
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path2.cgPath
expandAnimation.toValue = path1.cgPath
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
animationDirection = true
}
}
func createPathOne(){
path1.append(upLeftCircle);
path1.append(centerMainLine);
path1.append(downRightCircle)
}
func createPathTwo(){
path2.append(downLeftCircle);
path2.append(centerMainLine);
path2.append(upRightCircle)
}
func createPath() {
let frameSize = self.frame.size;
let lineHeight1 : CGFloat = 30
let lineHeight2 : CGFloat = 20
let radius1 : CGFloat = 40.0
let radius2 : CGFloat = 20.0
var lastPoint : CGPoint = CGPoint(x:0.0,y:frameSize.height/2 - lineHeight1/2)
for i in 1...10{
let p1 = drawUpCircle(centerPoint: CGPoint(x: lastPoint.x + radius1, y: lastPoint.y ), radiusValue: radius1)
lastPoint = p1.currentPoint;
let p2 = drawLine(fromPoint: lastPoint , toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y+lineHeight1))
lastPoint = p2.currentPoint;
let p3 = drawDownCircle(centerPoint: CGPoint(x:lastPoint.x + radius1, y: lastPoint.y), radiusValue: radius1)
lastPoint = p3.currentPoint;
let p4 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y - lineHeight2))
lastPoint = p4.currentPoint;
let p5 = drawUpCircle(centerPoint: CGPoint(x:lastPoint.x + radius2, y: lastPoint.y), radiusValue: radius2)
lastPoint = p5.currentPoint;
let p6 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y + lineHeight2))
lastPoint = p6.currentPoint;
let p7 = drawDownCircle(centerPoint: CGPoint(x:lastPoint.x + radius2, y: lastPoint.y), radiusValue: radius2)
lastPoint = p7.currentPoint
let p8 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y - lineHeight1))
lastPoint = p8.currentPoint;
path.append(p1)
path.append(p2)
path.append(p3)
path.append(p4)
path.append(p5)
path.append(p6)
path.append(p7)
path.append(p8)
}
}
}
Now run the code to check the Animation Loader. Comment/Uncomment other 2 loader method in viewDidAppear method of viewcontroller.
Enjoy!!
If someone is looking for Objective C version of #rob mayoff solution
in SpinnerView.h
#import <UIKit/UIKit.h>
IB_DESIGNABLE
#interface SpinnerView : UIView
#end
in SpinnerView.m
#import "SpinnerView.h"
#import "Pose.h"
#implementation SpinnerView
- (instancetype) initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
return self;
}
- (instancetype) initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
return self;
}
- (CAShapeLayer*) layer {
return (CAShapeLayer*)super.layer;
}
- (CAShapeLayer*) getLayer{
return (CAShapeLayer*)super.layer;
}
+ (Class)layerClass{
return [CAShapeLayer class];
}
- (void) layoutSubviews{
[super layoutSubviews];
[self getLayer].fillColor = nil;
[self getLayer].strokeColor = [UIColor blackColor].CGColor;
[self getLayer].lineWidth = 3;
[self setPath];
}
- (void) didMoveToWindow{
[self animate];
}
- (void) setPath{
UIBezierPath* bezierPath = ([UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, [self getLayer].lineWidth/2, [self getLayer].lineWidth/2)]);
[self getLayer].path = bezierPath.CGPath;
}
- (NSArray*) poses{
NSMutableArray* poses = [[NSMutableArray alloc] init];
[poses addObject:[[Pose alloc] initWith:0.0 start:0.000 length:0.7]];
[poses addObject:[[Pose alloc] initWith:0.6 start:0.500 length:0.5]];
[poses addObject:[[Pose alloc] initWith:0.6 start:1.000 length:0.3]];
[poses addObject:[[Pose alloc] initWith:0.6 start:1.500 length:0.1]];
[poses addObject:[[Pose alloc] initWith:0.2 start:1.875 length:0.1]];
[poses addObject:[[Pose alloc] initWith:0.2 start:2.250 length:0.3]];
[poses addObject:[[Pose alloc] initWith:0.2 start:2.625 length:0.7]];
[poses addObject:[[Pose alloc] initWith:0.2 start:3.000 length:0.5]];
return poses;
}
- (void) animate{
CFTimeInterval time = 0;
NSMutableArray* times = [NSMutableArray new];;
CGFloat start = 0;
NSMutableArray* rotations = [NSMutableArray new];
NSMutableArray* strokeEnds = [NSMutableArray new];
NSArray* posses = [self poses];
double totalSeconds = [[posses valueForKeyPath:#"#sum.secondsSincePriorPose"] doubleValue];
for(Pose* pose in posses){
time += pose.secondsSincePriorPose;
[times addObject:[NSNumber numberWithDouble:time/totalSeconds]];
start = pose.start;
[rotations addObject:[NSNumber numberWithDouble:start*2*M_PI]];
[strokeEnds addObject:[NSNumber numberWithDouble:pose.length]];
}
[times addObject:[times lastObject]];
[rotations addObject:[rotations firstObject]];
[strokeEnds addObject:[strokeEnds firstObject]];
[self animateKeyPath:#"strokeEnd" duration:totalSeconds times:times values:strokeEnds];
[self animateKeyPath:#"transform.rotation" duration:totalSeconds times:times values:rotations];
[self animateStrokeHueWithDuration:totalSeconds * 5];
}
- (void) animateKeyPath:(NSString*)keyPath duration:(CFTimeInterval)duration times:(NSArray*)times values:(NSArray*)values{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:keyPath];
animation.keyTimes = times;
animation.values = values;
animation.calculationMode = kCAAnimationLinear;
animation.duration = duration;
animation.repeatCount = FLT_MAX;
[[self getLayer] addAnimation:animation forKey:animation.keyPath];
}
- (void) animateStrokeHueWithDuration:(CFTimeInterval)duration{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"strokeColor"];
NSMutableArray *keyTimes = [NSMutableArray array];
NSMutableArray *values = [NSMutableArray array];
for (NSInteger i = 0; i < 36; i++) {
[keyTimes addObject: [NSNumber numberWithDouble:(CFTimeInterval)i/(CFTimeInterval)36]];
[values addObject:(id)[UIColor colorWithHue:(CGFloat)i/(CGFloat)36 saturation:1 brightness:1 alpha:1].CGColor];
}
animation.keyTimes = keyTimes;
animation.values = values;
animation.calculationMode = kCAAnimationLinear;
animation.duration = duration;
animation.repeatCount = FLT_MAX;
[[self getLayer] addAnimation:animation forKey:animation.keyPath];
}
#end
Pose.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#interface Pose : NSObject
#property CFTimeInterval secondsSincePriorPose;
#property CGFloat start;
#property CGFloat length;
- (instancetype) initWith:(CFTimeInterval)timeInterval start:(CGFloat)start length:(CGFloat)length;
#end
Pose.m
#import "Pose.h"
#import <UIKit/UIKit.h>
#implementation Pose
- (instancetype) initWith:(CFTimeInterval)timeInterval start:(CGFloat)start length:(CGFloat)length{
self = [super init];
self.start = start;
self.length = length;
self.secondsSincePriorPose = timeInterval;
return self;
}
#end

Resources