I have a scaling/pulsing animation that is working on cgPaths in a for loop as below. This code works but only when you append the animatedLayers to the circleLayer path when the circleLayer has already been added to the subLayer and this creates a static circle (glitch-like) before the animation (DispatchQueue) starts...
...
self.layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
...
Is it possible to add a CAShapeLayer with arguments to a subLayer? If not, any recommended alternative?
import UIKit
import Foundation
#IBDesignable
class AnimatedCircleView: UIView {
// MARK: - Initializers
var animatedLayers = [CAShapeLayer]()
// MARK: - Methods
override func draw(_ rect: CGRect) {
// Animated circle
for _ in 0...3 {
let animatedPath = UIBezierPath(arcCenter: .zero, radius: self.layer.bounds.size.width / 2.3,
startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let animatedLayer = CAShapeLayer()
animatedLayer.path = animatedPath.cgPath
animatedLayer.strokeColor = UIColor.black.cgColor
animatedLayer.lineWidth = 0
animatedLayer.fillColor = UIColor.gray.cgColor
animatedLayer.lineCap = CAShapeLayerLineCap.round
animatedLayer.position = CGPoint(x: self.layer.bounds.size.width / 2, y: self.layer.bounds.size.width / 2)
self.layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
}
// Dispatch animation for circle _ 0...3
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animateCircle(index: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.animateCircle(index: 1)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.animateCircle(index: 2)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.animateCircle(index: 3)
}
}
}
}
}
func animateCircle(index: Int) {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 1.8
scaleAnimation.fromValue = 0
scaleAnimation.toValue = 1
scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
scaleAnimation.repeatCount = Float.infinity
animatedLayers[index].add(scaleAnimation, forKey: "scale")
let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
opacityAnimation.duration = 1.8
opacityAnimation.fromValue = 0.7
opacityAnimation.toValue = 0
opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
opacityAnimation.repeatCount = Float.infinity
animatedLayers[index].add(opacityAnimation, forKey: "opacity")
}
}
The key issue is that your animations start, with staggered delays, between in 0.1 and 1.0 seconds. Until that last animation starts, that layer is just sitting there, full sized and at 100% opacity.
Since you are animating the transform scale from 0 to 1, I’d suggest setting the starting transform to 0 (or changing the opacity to 0). Then you won’t see them sitting there until their respective animations start.
A few other observations:
The draw(_:) is not the right place to add layers, start animations, etc. This method may be called multiple times and should represent the view at a given point in time. I’d retire draw(_:) is it’s not the right place to start this and you don’t need this method at all.
You start your first animation after 0.1 seconds. Why not start it immediately?
You should handle frame adjustments by deferring the setting of the path and position properties until layoutSubviews.
Thus:
#IBDesignable
class AnimatedCircleView: UIView {
private var animatedLayers = [CAShapeLayer]()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(arcCenter: .zero, radius: bounds.width / 2.3, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
for animatedLayer in animatedLayers {
animatedLayer.path = path.cgPath
animatedLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
}
}
}
// MARK: - Methods
private extension AnimatedCircleView {
func configure() {
for _ in 0...3 {
let animatedLayer = CAShapeLayer()
animatedLayer.strokeColor = UIColor.black.cgColor
animatedLayer.lineWidth = 0
animatedLayer.fillColor = UIColor.gray.cgColor
animatedLayer.lineCap = .round
animatedLayer.transform = CATransform3DMakeScale(0, 0, 1)
layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
}
self.animateCircle(index: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animateCircle(index: 1)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.animateCircle(index: 2)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.animateCircle(index: 3)
}
}
}
}
func animateCircle(index: Int) {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 1.8
scaleAnimation.fromValue = 0
scaleAnimation.toValue = 1
scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
scaleAnimation.repeatCount = .greatestFiniteMagnitude
animatedLayers[index].add(scaleAnimation, forKey: "scale")
let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
opacityAnimation.duration = 1.8
opacityAnimation.fromValue = 0.7
opacityAnimation.toValue = 0
opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
opacityAnimation.repeatCount = .greatestFiniteMagnitude
animatedLayers[index].add(opacityAnimation, forKey: "opacity")
}
}
Have you tried starting with the fillcolor as clear then turning it to grey at the start of the animation?
// Animated circle
for _ in 0...3 {
let animatedPath = UIBezierPath(arcCenter: .zero, radius: self.layer.bounds.size.width / 2.3,
startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let animatedLayer = CAShapeLayer()
animatedLayer.path = animatedPath.cgPath
animatedLayer.strokeColor = UIColor.black.cgColor
animatedLayer.lineWidth = 0
animatedLayer.fillColor = UIColor.clear.cgColor
animatedLayer.lineCap = CAShapeLayerLineCap.round
animatedLayer.position = CGPoint(x: self.layer.bounds.size.width / 2, y: self.layer.bounds.size.width / 2)
self.layer.addSublayer(animatedLayer)
animatedLayers.append(animatedLayer)
}
// Dispatch animation for circle _ 0...3
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animateCircle(index: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.animateCircle(index: 1)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.animateCircle(index: 2)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.animateCircle(index: 3)
}
}
}
}
}
Related
How can I make this loading indicator using CABasicAnimation?
I was trying to do like this
but it's working not correctly, I am not sure that adding four layers with specific paths and animations is a good idea. I need to make exactly the same I see on the picture. I hope, it's doable using only one layer with some magic
private func setupView() {
let firstLayer = createLayer()
let secondLayer = createLayer()
let thirdLayer = createLayer()
let fourthLayer = createLayer()
firstLayer.path = createBezierPath(
layerWidth: firstLayer.lineWidth,
startAngle: 0,
endAngle: 0.25
).cgPath
secondLayer.path = createBezierPath(
layerWidth: secondLayer.lineWidth,
startAngle: 0.25,
endAngle: 0.5
).cgPath
thirdLayer.path = createBezierPath(
layerWidth: thirdLayer.lineWidth,
startAngle: 0.5,
endAngle: 0.75
).cgPath
fourthLayer.path = createBezierPath(
layerWidth: thirdLayer.lineWidth,
startAngle: 0.75,
endAngle: 1
).cgPath
firstLayer.add(createAnimation(), forKey: "firstLayer")
secondLayer.add(createAnimation(), forKey: "secondLayer")
thirdLayer.add(createAnimation(), forKey: "thirdLayer")
fourthLayer.add(createAnimation(), forKey: "thirdLayer")
self.layer.addSublayer(firstLayer)
self.layer.addSublayer(secondLayer)
self.layer.addSublayer(thirdLayer)
self.layer.addSublayer(fourthLayer)
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.beginTime = 0
animation.duration = 1
animation.fromValue = CGFloat.angle(progress: 0)
animation.toValue = CGFloat.angle(progress: 0.25)
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
animation.fillMode = .forwards
animation.repeatDuration = .infinity
self.layer.add(animation, forKey: "mainAnim")
}
private func createBezierPath(
layerWidth: CGFloat,
startAngle: CGFloat,
endAngle: CGFloat
) -> UIBezierPath {
return .init(
arcCenter: CGPoint(x: bounds.width / 2, y: bounds.height / 2),
radius: (bounds.height - layerWidth) / 2,
startAngle: .angle(progress: startAngle),
endAngle: .angle(progress: endAngle),
clockwise: true
)
}
private func createAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.beginTime = 0
animation.duration = 1
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.repeatDuration = .infinity
return animation
}
private func createLayer() -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.orange.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 5
shapeLayer.lineCap = .round
shapeLayer.strokeEnd = 0
return shapeLayer
}
private extension CGFloat {
static var initialAngle: CGFloat = -(.pi / 2)
static func angle(progress: CGFloat) -> CGFloat {
.pi * 2 * progress + .initialAngle
}
}
I'd use a motion design tool to recreate that. For example, I think you could create this animation with the free version of Flow, which can export Swift code or Lottie files.
I'm trying to animate a circular progress timer animation for a timer app, but the timer counts and the animation never starts.
import UIKit
class TimerPage: UIViewController {
let timeLeftShapeLayer = CAShapeLayer()
let bgShapeLayer = CAShapeLayer()
var timeLeft: TimeInterval = 60
var endTime: Date?
var timeLabel = UILabel()
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.94, alpha: 1.0)
drawBgShape()
drawTimeLeftShape()
addTimeLabel()
// here you define the fromValue, toValue and duration of your animation
strokeIt.fromValue = 0
strokeIt.toValue = 1
strokeIt.duration = 60
// add the animation to your timeLeftShapeLayer
timeLeftShapeLayer.add(strokeIt, forKey: nil)
// define the future end time by adding the timeLeft to now Date()
endTime = Date().addingTimeInterval(timeLeft)
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
}
// here you create your basic animation object to animate the strokeEnd
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
150, startAngle: (3*CGFloat.pi) / 2, endAngle: -CGFloat.pi / 2, clockwise: false).cgPath
bgShapeLayer.strokeColor = UIColor.blue.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 15
view.layer.addSublayer(bgShapeLayer)
}
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: (3*CGFloat.pi) / 2, endAngle: -CGFloat.pi / 2, clockwise: false).cgPath
timeLeftShapeLayer.strokeColor = UIColor.white.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 15
view.layer.addSublayer(timeLeftShapeLayer)
}
func addTimeLabel() {
timeLabel = UILabel(frame: CGRect(x: view.frame.midX-50 ,y: view.frame.midY-25, width: 100, height: 50))
timeLabel.textAlignment = .center
timeLabel.text = timeLeft.time
view.addSubview(timeLabel)
}
#objc func updateTime() {
if timeLeft > 0 {
timeLeft = endTime?.timeIntervalSinceNow ?? 0
timeLabel.text = timeLeft.time
} else {
timeLabel.text = "00:00"
timer.invalidate()
}
}
}
extension TimeInterval {
var time: String {
return String(format:"%02d:%02d", Int(self/60), Int(ceil(truncatingRemainder(dividingBy: 60))) )
}
}
I tested this out on a separate test project by itself, and it works perfectly fine. But when I add it to my actual app as part of a segue, the animation never starts.
I'm trying to create pulse animation rectangular shape
I have code:
func createPulse() {
for _ in 0...3 {
let circularPath = UIBezierPath(arcCenter: .zero, radius: view.bounds.size.height/2, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
let pulseLayer = CAShapeLayer()
pulseLayer.path = circularPath.cgPath
pulseLayer.lineWidth = 2
pulseLayer.fillColor = UIColor.clear.cgColor
pulseLayer.lineCap = CAShapeLayerLineCap.round
pulseLayer.position = CGPoint(x: binauralBackView.frame.size.width/2, y: binauralBackView.frame.size.height/2)
pulseLayer.cornerRadius = binauralBackView.frame.width/2
binauralBackView.layer.insertSublayer(pulseLayer, at: 0)
pulseLayers.append(pulseLayer)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animatePulse(index: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.animatePulse(index: 1)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.animatePulse(index: 2)
}
}
}
}
func animatePulse(index: Int) {
pulseLayers[index].strokeColor = #colorLiteral(red: 0.7215686275, green: 0.5137254902, blue: 0.7647058824, alpha: 1).cgColor
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 2
scaleAnimation.fromValue = 0.0
scaleAnimation.toValue = 0.9
scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
scaleAnimation.repeatCount = .greatestFiniteMagnitude
pulseLayers[index].add(scaleAnimation, forKey: "scale")
let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
opacityAnimation.duration = 2
opacityAnimation.fromValue = 0.9
opacityAnimation.toValue = 0.0
opacityAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
opacityAnimation.repeatCount = .greatestFiniteMagnitude
pulseLayers[index].add(opacityAnimation, forKey: "opacity")
}
and have result
circle result
I need to create rectangular like this (like border of image):
rectangular
How can change circle to rectangular?
Output:
Note: You can add CABasicAnimation for opacity too in animationGroup if this solution works for you.
extension UIView {
/// animate the border width
func animateBorderWidth(toValue: CGFloat, duration: Double) -> CABasicAnimation {
let widthAnimation = CABasicAnimation(keyPath: "borderWidth")
widthAnimation.fromValue = layer.borderWidth
widthAnimation.toValue = toValue
widthAnimation.duration = duration
return widthAnimation
}
/// animate the scale
func animateScale(toValue: CGFloat, duration: Double) -> CABasicAnimation {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1.0
scaleAnimation.toValue = toValue
scaleAnimation.duration = duration
scaleAnimation.repeatCount = .infinity
scaleAnimation.isRemovedOnCompletion = false
scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
return scaleAnimation
}
func pulsate(animationDuration: CGFloat) {
var animationGroup = CAAnimationGroup()
animationGroup = CAAnimationGroup()
animationGroup.duration = animationDuration
animationGroup.repeatCount = Float.infinity
let newLayer = CALayer()
newLayer.bounds = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
newLayer.cornerRadius = self.frame.width/2
newLayer.position = CGPoint(x: self.frame.width/2, y: self.frame.height/2)
newLayer.cornerRadius = self.frame.width/2
animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.default)
animationGroup.animations = [animateScale(toValue: 6.0, duration: 2.0),
animateBorderWidth(toValue: 1.2, duration: animationDuration)]
newLayer.add(animationGroup, forKey: "pulse")
self.layer.cornerRadius = self.frame.width/2
self.layer.insertSublayer(newLayer, at: 0)
}
}
Usage:
class ViewController: UIViewController {
#IBOutlet weak var pulseView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
pulseView.pulsate(animationDuration: 1.0)
}
}
Is there a method or function that can stop previous animation, when I switch segment controls?
If I don't remove animation, image that was fading on first segment, starts rotating on the second and then moves on the third segment.
While I switch segments, it works more like appending different animations all the time.
I tried:
- removeAllAnimations(), but it doesn't work properly.
- self.view.layer.removeAnimation(forKey: "moveAnimation"). Tried that one at the end of the code for each segment choice.
Maybe I am not putting it in the right spot, but I can't get it working properly
Here is my code:
import UIKit
class MoveViewController: UIViewController {
#IBOutlet var sgAction : UISegmentedControl!
var moveLayer : CALayer?
#IBAction func segmentDidChange(sender : UISegmentedControl){
updateAction()
}
func updateAction(){
let action = sgAction.selectedSegmentIndex
if action == 0 {
//fade
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
self.view.layer.removeAnimation(forKey: "fadeAnimation")
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
fadeAnimation.fromValue = NSNumber.init(value: 1.0)
fadeAnimation.toValue = NSNumber.init(value: 0.0)
fadeAnimation.isRemovedOnCompletion = false
fadeAnimation.duration = 3.0
fadeAnimation.beginTime = 1.0
fadeAnimation.isAdditive = false
fadeAnimation.fillMode = CAMediaTimingFillMode.both
fadeAnimation.repeatCount = Float.infinity
moveLayer?.add(fadeAnimation, forKey: nil)
} else if action == 1 {
//2. rotate
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
self.view.layer.removeAnimation(forKey: "rotateAnimation")
rotateAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
rotateAnimation.fromValue = 0
rotateAnimation.toValue = 2 * Double.pi
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = 1.0
rotateAnimation.repeatCount = Float.infinity
moveLayer?.add(rotateAnimation, forKey:nil)
} else {
//3. move
let moveAnimation = CABasicAnimation(keyPath: "position")
self.view.layer.removeAnimation(forKey: "moveAnimation")
moveAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
moveAnimation.fromValue = NSValue.init(cgPoint: CGPoint(x: 0, y: 0))
moveAnimation.toValue = NSValue.init(cgPoint: CGPoint(x: 700, y: 500))
moveAnimation.isRemovedOnCompletion = false
moveAnimation.duration = 3.0
moveAnimation.repeatCount = Float.infinity
moveLayer?.add(moveAnimation, forKey: nil)
}
}
// moveLayer?.removeAllAnimations()
func createImg(){
let displayImage = UIImage(named: "balloon.png")
moveLayer = CALayer.init()
moveLayer?.contents = displayImage?.cgImage
moveLayer?.bounds = CGRect(x: 0, y: 0, width: 150, height: 150)
moveLayer?.position = CGPoint(x: 300, y: 200)
self.view.layer.addSublayer(moveLayer!)
}
override func viewDidLoad() {
super.viewDidLoad()
createImg()
// moveLayer?.removeAllAnimations()
updateAction()
}
}
When you add moveLayer?.removeAllAnimations() and self.moveLayer.layoutIfNeed() to the segmentDidChange(sender : UISegmentedControl). Also the other key item that needs to get changed is moveLayer?.add(fadeAnimation, forKey: "nil") to moveLayer?.add(fadeAnimation, forKey: "fadeAnimation").
import UIKit
class MoveViewController: UIViewController {
#IBOutlet var sgAction : UISegmentedControl!
var moveLayer : CALayer?
override func viewDidLoad() {
super.viewDidLoad()
createImg()
updateAction()
}
#IBAction func segmentDidChange(sender : UISegmentedControl){
moveLayer?.removeAllAnimations()
self.moveLayer?.layoutIfNeeded()
updateAction()
}
func updateAction(){
switch sgAction.selectedSegmentIndex {
case 0:
//fade
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
fadeAnimation.fromValue = NSNumber.init(value: 1.0)
fadeAnimation.toValue = NSNumber.init(value: 0.0)
fadeAnimation.isRemovedOnCompletion = false
fadeAnimation.duration = 3.0
fadeAnimation.beginTime = 1.0
fadeAnimation.isAdditive = false
fadeAnimation.fillMode = CAMediaTimingFillMode.both
fadeAnimation.repeatCount = Float.infinity
moveLayer?.add(fadeAnimation, forKey: "fadeAnimation")
case 1:
//2. rotate
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
rotateAnimation.fromValue = 0
rotateAnimation.toValue = 2 * Double.pi
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = 1.0
rotateAnimation.repeatCount = Float.infinity
moveLayer?.add(rotateAnimation, forKey: "rotateAnimation")
case 2:
//3. move
let moveAnimation = CABasicAnimation(keyPath: "position")
moveAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
moveAnimation.fromValue = NSValue.init(cgPoint: CGPoint(x: 0, y: 0))
moveAnimation.toValue = NSValue.init(cgPoint: CGPoint(x: 700, y: 500))
moveAnimation.isRemovedOnCompletion = false
moveAnimation.duration = 3.0
moveAnimation.repeatCount = Float.infinity
moveLayer?.add(moveAnimation, forKey: "position")
default: break
}
}
func createImg(){
let displayImage = UIImage(named: "balloon.png")
moveLayer = CALayer.init()
moveLayer?.contents = displayImage?.cgImage
moveLayer?.bounds = CGRect(x: 0, y: 0, width: 150, height: 150)
moveLayer?.position = CGPoint(x: 300, y: 200)
self.view.layer.addSublayer(moveLayer!)
}
}
I am trying to have an animation in a view controller in which the circle rotates with animation. The circle should rotate until a process completed like a gif below. I have implemented the circle animation but couldn't reach to the point what I want to achieve.
import UIKit
class ViewController: UIViewController {
var circle : Circle?;
override func viewDidLoad() {
super.viewDidLoad();
view.backgroundColor = UIColor.white;
setupViews();
}
func setupViews(){
circle = Circle(frame: self.view.frame);
view.addSubview(circle!);
circle?.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true;
circle?.topAnchor.constraint(equalTo: view.topAnchor).isActive = true;
circle?.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true;
circle?.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true;
}
}
class Circle : UIView{
override init(frame: CGRect) {
super.init(frame: frame);
self.backgroundColor = .blue;
self.translatesAutoresizingMaskIntoConstraints = false;
setupCircle();
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let gradientLayer = CAGradientLayer();
func setupCircle(){
layer.addSublayer(shapeLayer);
let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.frame.width / 2 - 50, y: self.frame.height / 2 - 50), radius: 50, startAngle: CGFloat(Double.pi * (0 / 4)), endAngle: CGFloat(Double.pi * 2), clockwise: true);
shapeLayer.path = circlePath.cgPath;
let group = CAAnimationGroup()
group.animations = [animateStrokeEnd, animateOpacity]
group.duration = 0.8
group.repeatCount = HUGE // repeat forver
shapeLayer.add(group, forKey: nil)
}
let shapeLayer: CAShapeLayer = {
let layer = CAShapeLayer();
layer.strokeColor = UIColor.white.cgColor;
layer.lineWidth = 5;
layer.fillColor = UIColor.clear.cgColor;
layer.strokeStart = 0
layer.strokeEnd = 1;
return layer;
}();
let animateOpacity : CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "opacity");
animation.fromValue = 0;
animation.toValue = 0.8;
animation.byValue = 0.01;
animation.repeatCount = Float.infinity;
return animation
}();
let animateStrokeEnd: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "strokeEnd");
animation.fromValue = 0;
animation.repeatCount = Float.infinity;
animation.toValue = 1;
return animation;
}();
}
I am using strokeEnd animation to implement the animation. And opacity to animate the color. But when the circle reaches 360 degree, its makes a lag before starting a new circle.
Does anybody know how to remove this effect and get smooth animation?
The above code produces this animation
But i want to achieve this animation
Also the stroke colour is different from the original animation. Can we achieve this animation using the CABasicAnimation?
Rather than trying to animate the actual drawing, just draw the view once and then animate it.
Here is a custom PadlockView and a custom CircleView which mimic the animation you showed. To use it, add the code below to your project. Add a UIView to your Storyboard, change its class to PadlockView, and make an #IBOutlet to it (called padlock perhaps). When you want the view to animate, set padlock.circle.isAnimating = true. To stop animating, set padlock.circle.isAnimating = false.
CircleView.swift
// This UIView extension was borrowed from #keval's answer:
// https://stackoverflow.com/a/41160100/1630618
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 3) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat.pi * 2
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
}
class CircleView: UIView {
var foregroundColor = UIColor.white
var lineWidth: CGFloat = 3.0
var isAnimating = false {
didSet {
if isAnimating {
self.isHidden = false
self.rotate360Degrees(duration: 1.0)
} else {
self.isHidden = true
self.layer.removeAllAnimations()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.isHidden = true
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
let width = bounds.width
let height = bounds.height
let radius = (min(width, height) - lineWidth) / 2.0
var currentPoint = CGPoint(x: width / 2.0 + radius, y: height / 2.0)
var priorAngle = CGFloat(360)
for angle in stride(from: CGFloat(360), through: 0, by: -2) {
let path = UIBezierPath()
path.lineWidth = lineWidth
path.move(to: currentPoint)
currentPoint = CGPoint(x: width / 2.0 + cos(angle * .pi / 180.0) * radius, y: height / 2.0 + sin(angle * .pi / 180.0) * radius)
path.addArc(withCenter: CGPoint(x: width / 2.0, y: height / 2.0), radius: radius, startAngle: priorAngle * .pi / 180.0 , endAngle: angle * .pi / 180.0, clockwise: false)
priorAngle = angle
foregroundColor.withAlphaComponent(angle/360.0).setStroke()
path.stroke()
}
}
}
PadlockView.swift
class PadlockView: UIView {
var circle: CircleView!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.backgroundColor = .clear
circle = CircleView()
circle.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(circle)
circle.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
circle.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
circle.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
circle.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
}
override func draw(_ rect: CGRect) {
let width = bounds.width
let height = bounds.height
let lockwidth = width / 3
let lockheight = height / 4
let boltwidth = lockwidth * 2 / 3
UIColor.white.setStroke()
let path = UIBezierPath()
path.move(to: CGPoint(x: (width - lockwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width + lockwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width + lockwidth) / 2, y: height / 2 + lockheight))
path.addLine(to: CGPoint(x: (width - lockwidth) / 2, y: height / 2 + lockheight))
path.close()
path.move(to: CGPoint(x: (width - boltwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width - boltwidth) / 2, y: height / 2 - boltwidth / 4))
path.addArc(withCenter: CGPoint(x: width/2, y: height / 2 - boltwidth / 4), radius: boltwidth / 2, startAngle: .pi, endAngle: 0, clockwise: true)
path.lineWidth = 2.0
path.stroke()
}
}
Note: Continuous animation code courtesy of this answer.
Here is a demo that I setup with the following code in my ViewController:
#IBOutlet weak var padlock: PadlockView!
#IBAction func startStop(_ sender: UIButton) {
if sender.currentTitle == "Start" {
sender.setTitle("Stop", for: .normal)
padlock.circle.isAnimating = true
} else {
sender.setTitle("Start", for: .normal)
padlock.circle.isAnimating = false
}
}