I'm trying to create an indefinite loader where a bar moves from left to right. It's kind of like the spinner you see in Google Apps, but then a bar. This blog is a great post to show how a spinner like that can be created using UIBezierPaths and CABasicAnimation:
http://blog.matthewcheok.com/design-teardown-spinning-indicator/
But in my case, I want to do the same with a bar. So there's a line that goes from left to right, and like in the spinner, I also animate the strokeStart with a small delay such that it seems to follow the line and catch up with it at the end. And when it reaches the end, I want it to go back from right to left, etcetera. All of this using easeIn and easeOut. And I don't want the line to be invisible when it reaches the end, a small part should always remain visible.
To make my explanation much more clear, this is what I'm going for:
I know it seems like I already have want I want, but I created this gif by using ugly delay-code that don't work correctly half of the time.
So, to continue, the auto-reverse property of the CABasicAnimation can't be used because I animate both strokeEnd and strokeStart, so I had two ideas:
Now, I got quite far, but the tricky part is when I have to switch the layers around. I always see a flicker, whatever I try. I know I have to use the presentation layer and set the right values before or after adding the animated group.
I use the CATransaction.setCompletionBlock to know when either the left or right part has been reached, and then I turn the other way.
Here are some parts of code I've written:
UIBezierPaths:
let start:CGPoint = .init(x: 0, y: 0)
let end:CGPoint = .init(x: frame.width, y: 0)
let pathLeft = UIBezierPath()
pathLeft.move(to: start)
pathLeft.addLine(to: end)
self.barLayerLeft.path = pathLeft.cgPath
let pathRight = UIBezierPath()
pathRight.move(to: end)
pathRight.addLine(to: start)
self.barLayerRight.path = pathRight.cgPath
Setup strokeStart and strokeEnd animations and a group
let strokeStartAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "strokeStart")
animation.fromValue = 0
animation.toValue = 0.99
animation.beginTime = 0.3
animation.duration = 1
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
return animation
}()
let strokeEndAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.beginTime = 0
animation.duration = 1.3
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
return animation
}()
Group & layers
let groupedAnimation:CAAnimationGroup = CAAnimationGroup()
groupedAnimation.duration = 1.4
groupedAnimation.animations = [self.strokeStartAnimation, self.strokeEndAnimation]
self.barLayerLeft.lineWidth = 4
self.barLayerLeft.lineCap = .round
self.barLayerLeft.strokeColor = AppStyler.mainColor.cgColor
self.layer.addSublayer(self.barLayerLeft)
self.barLayerRight.lineWidth = 4
self.barLayerRight.lineCap = .round
self.barLayerRight.strokeColor = AppStyler.mainColor.cgColor
self.layer.addSublayer(self.barLayerRight)
Animate left (incl. setting the final positions so the animated line will stay exactly where it was when the animation finished, and removing the right-to-left bar)
self.barLayerLeft.strokeEnd = 1
self.barLayerLeft.strokeStart = 0.01
self.barLayerRight.strokeEnd = 1
self.barLayerRight.strokeStart = 0
self.barLayerLeft.add(self.groupedAnimation, forKey: "groupLeft")
CATransaction.setCompletionBlock{ [weak self] in
self.animateRight()
}
Animate right (and incl. positions like above)
self.barLayerRight.strokeEnd = 1
self.barLayerRight.strokeStart = 0.01
self.barLayerLeft.strokeEnd = 1
self.barLayerLeft.strokeStart = 0
self.barLayerRight.add(self.groupedAnimation, forKey: "groupRight")
CATransaction.setCompletionBlock{ [weak self] in
self.animateLeft()
}
I've already tried using 'CATransaction.setDisableActions' to switch the layers without seeing a flicker like this:
CATransaction.begin()
CATransaction.setDisableActions(true)
self.barLayerLeft.strokeEnd = 0.01
self.barLayerLeft.strokeStart = 0
self.barLayerRight.strokeEnd = 1
self.barLayerRight.strokeStart = 0
CATransaction.commit()
But I can't get it to work...
So, my main question is. Can my ideas above actually work without flickering? Or do I need to use something else entirely...? Is there a way easier/better way to achieve what I want?
Btw, if anything is unclear or if I need to post more code, let me know!
I would approach this using masking and a single moving layer:
let container = CALayer()
container.frame = CGRect(x: 10, y: 10, width: 200, height: 10)
container.borderColor = UIColor.black.cgColor
container.borderWidth = 1
container.cornerRadius = 5
container.masksToBounds = true
view.layer.addSublayer(container)
let bar = CALayer()
bar.frame = container.bounds
bar.anchorPoint = CGPoint(x: 0, y: 0.5)
bar.position = CGPoint(x: -195, y: 5)
bar.backgroundColor = UIColor.blue.cgColor
container.addSublayer(bar)
let animation = CABasicAnimation(keyPath: "position.x")
animation.fromValue = bar.position.x
animation.toValue = 195
animation.autoreverses = true
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 2
bar.add(animation, forKey: nil)
Looks like this, but less terrible because it's not a GIF:
This would be much simpler to pause at the end of your work, as it's a single animation.
For a bar that doesn't fill the whole area, change the frame of the bar:
bar.frame = CGRect(x: 0, y: 0, width: 75, height: 10)
And the start position:
bar.position = CGPoint(x: -70, y: 5)
Which gives you:
You can round the edges of the moving bar like so:
bar.cornerRadius = 5