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
}
}