Add shimmer animation flowing from one view to another - ios

I am trying to create shimmer animation flowing from one view to another view. Below is the code that I have so far:
override func viewDidLoad() {
super.viewDidLoad()
view1.backgroundColor = .purple
view2.backgroundColor = .purple
view1.startAnimating()
//let seconds = 2.0
//DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
self.view2.startAnimating()
//}
}
class ShimmerView: UIView {
let gradientColorOne : CGColor = UIColor.purple.cgColor
let gradientColorTwo : CGColor = UIColor.yellow.cgColor
func addGradientLayer() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.colors = [gradientColorOne, gradientColorTwo, gradientColorOne]
gradientLayer.locations = [0.0, 0.5, 1.0]
self.layer.addSublayer(gradientLayer)
return gradientLayer
}
func addAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = .infinity
animation.duration = 2
return animation
}
func startAnimating() {
let gradientLayer = addGradientLayer()
let animation = addAnimation()
gradientLayer.add(animation, forKey: animation.keyPath)
}
}
I tried to add delay but I guess that's not going to work because next time the animation should start with further delay.
This is how it behaves right now. Animation happens in parallel:
But, what I want is that it to start the animation from view1 and then finishes in view2 and it keep on doing that. Any ideas/ suggestions on how to achieve this? Thank you.

You can try controlling the animation sequencing from call site like following.
Define a constant called animationDuration inside the ShimmerView like this.
public static let animationDuration: Double = 2.0
Update the addAnimation() implementation like this.
// Remove this line, We need this to be performed only once at a time.
/*
animation.repeatCount = .infinity
*/
// Update this line to use the constant defined earlier
animation.duration = ShimmerView.animationDuration
From your call site, you can now do this.
class ViewController: UIViewController {
let view1 = ShimmerView()
let view2 = ShimmerView()
func startAnimating() {
view1.startAnimating()
let interval = ShimmerView.animationDuration
DispatchQueue.main.asyncAfter(deadline: .now() + interval) { [weak self] in
self?.view2.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + interval) { [weak self] in
self?.startAnimating()
}
}
}
}
UPDATE
This kind of almost works, but when view1 finishes the animation and view2 starts it, the gradient on view1 sits in the middle and vice versa. So, it doesn't give a clear flowing animation from top to bottom that goes on.
The problem is your code is not removing the gradientLayer instance from your view upon animation completion. You can do it with CATransaction.setCompletionBlock like following.
import Foundation
import UIKit
class ShimmerView: UIView {
static let animationDuration: Double = 2.0
let gradientColorOne: CGColor = UIColor.purple.cgColor
let gradientColorTwo: CGColor = UIColor.yellow.cgColor
lazy var gradientLayer: CAGradientLayer = {
let gradient = CAGradientLayer()
gradient.startPoint = CGPoint(x: 0.0, y: 0.0)
gradient.endPoint = CGPoint(x: 0.0, y: 1.0)
gradient.colors = [gradientColorOne, gradientColorTwo, gradientColorOne]
gradient.locations = [0.0, 0.5, 1.0]
return gradient
}()
func addGradientLayer() {
gradientLayer.frame = self.bounds
self.layer.addSublayer(gradientLayer)
}
func removeGradientLayer() {
gradientLayer.removeFromSuperlayer()
}
func addAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.duration = ShimmerView.animationDuration
return animation
}
func startAnimating() {
self.addGradientLayer()
let animation = addAnimation()
CATransaction.begin()
// Setting this block before adding the animation is crucial
CATransaction.setCompletionBlock({ [weak self] in
self?.removeGradientLayer()
})
gradientLayer.add(animation, forKey: animation.keyPath)
CATransaction.commit()
}
}

Related

CABasicAnimation not working on CAGradientLayer

I'm trying to create a Shimmer effect to a UIButton, so i've subclassed it and tried to start the animation, but nothing happens. I can see the gradient on the button, but it is not animating. What am I doing wrong?
This is how it looks:
Here is my code:
class ShimmerButton: UIButton {
private var gradientColorOne : CGColor = UIColor(white: 0.85, alpha: 0.0).cgColor
private var gradientColorTwo : CGColor = UIColor(white: 0.95, alpha: 0.5).cgColor
func addGradientLayer() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.colors = [self.gradientColorOne, self.gradientColorTwo, self.gradientColorOne]
gradientLayer.locations = [0.0, 0.5, 1.0]
let top = self.layer.sublayers?.last
self.layer.insertSublayer(gradientLayer, above: top)
return gradientLayer
}
func addAnimation(duration: Double, repeatCount: Float) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = repeatCount
animation.duration = duration
return animation
}
func startAnimating(duration: Double, repeatCount: Float) {
let gradientLayer = self.addGradientLayer()
let animation = self.addAnimation(duration: duration, repeatCount: repeatCount)
gradientLayer.add(animation, forKey: "anim")
}
}
Starting the animation:
self.button.startAnimating(duration: 0.9, repeatCount: .infinity)
Code sample has been taken from here.
Animating the positions property works just fine.
I wrote a playground that creates a simple gradient animation. It looks like this:
I also incorporated the OPs ShimmerButton (With some changes to make it so you can start/stop the shimmer animation. The shimmer animation looks like this:
Here is the code for that playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class ShimmerButton: UIButton {
public var animating = false
private var gradientLayer: CAGradientLayer?
private var gradientColorOne : CGColor = UIColor(white: 0.85, alpha: 0.0).cgColor
private var gradientColorTwo : CGColor = UIColor(white: 0.95, alpha: 0.5).cgColor
private func addGradientLayer() -> CAGradientLayer? {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.colors = [self.gradientColorOne, self.gradientColorTwo, self.gradientColorOne]
gradientLayer.locations = [-1.0, -0.5, 0.0]
let top = self.layer.sublayers?.last
self.layer.insertSublayer(gradientLayer, above: top)
return gradientLayer
}
public func addAnimation(duration: Double, repeatCount: Float) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = repeatCount
animation.duration = duration
animation.delegate = self
return animation
}
public func stopAnimating() {
guard let gradientLayer = gradientLayer else { return }
gradientLayer.removeFromSuperlayer()
gradientLayer.removeAllAnimations()
self.gradientLayer = nil
animating = false
}
public func startAnimating(duration: Double, repeatCount: Float) {
if !animating {
animating = true
gradientLayer = self.addGradientLayer()
let animation = self.addAnimation(duration: duration, repeatCount: repeatCount)
gradientLayer?.add(animation, forKey: "anim")
}
}
}
extension ShimmerButton: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished: Bool) {
animating = false
if finished {
gradientLayer?.removeFromSuperlayer()
gradientLayer = nil;
}
}
}
class GradientView: UIView {
var animationIsLeft: Bool = false
var animating = false
static override var layerClass: AnyClass {
return CAGradientLayer.self
}
public func animateGradient() {
animating = true
animateGradientStep()
}
public func animateGradientStep() {
guard animating,
let layer = layer as? CAGradientLayer else { return }
let animation = CABasicAnimation(keyPath:"locations")
animation.duration = 0.5
animationIsLeft = !animationIsLeft
animation.fromValue = animationIsLeft ? [0, 0.1, 1] : [0, 0.9, 1.0]
let toValue = !animationIsLeft ? [0, 0.1, 1] : [0, 0.9, 1.0]
animation.toValue = toValue
animation.delegate = self
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
layer.locations = toValue.map { NSNumber(value: $0) }
}
layer.add(animation, forKey: nil)
}
func doInitSetup() {
guard let layer = layer as? CAGradientLayer else { return }
layer.startPoint = CGPoint(x: 0.0, y: 1.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
layer.colors = [UIColor.blue.cgColor, UIColor.red.cgColor, UIColor.blue.cgColor]
layer.locations = [0, 0.1, 1]
}
override init(frame: CGRect) {
super.init(frame: frame)
doInitSetup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
doInitSetup()
}
}
extension GradientView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
animateGradientStep()
}
}
class MyViewController : UIViewController {
var gradientView: GradientView?
var button: ShimmerButton?
override func loadView() {
let view = UIView()
view.backgroundColor = .white
view.layer.borderWidth = 1
self.view = view
gradientView = GradientView()
if let gradientView = gradientView {
gradientView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(gradientView)
gradientView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
gradientView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100).isActive = true
gradientView.heightAnchor.constraint(equalToConstant: 200).isActive = true
gradientView.widthAnchor.constraint(equalToConstant: 200).isActive = true
gradientView.layer.borderWidth = 1
}
button = ShimmerButton()
guard let button = button else { return }
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
button.setTitle("Button", for: .normal)
button.backgroundColor = UIColor.lightGray
button.layer.borderWidth = 1
button.layer.cornerRadius = 10
button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
button.widthAnchor.constraint(equalToConstant: 120).isActive = true
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#IBAction func buttonTapped(_ sender: UIButton) {
if gradientView?.animating == true {
gradientView?.animating = false
} else {
gradientView?.animateGradient()
}
guard let button = button else { return }
if button.animating == true {
button.stopAnimating()
} else {
button.startAnimating(duration: 1.0, repeatCount: Float.greatestFiniteMagnitude)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
}
PlaygroundPage.current.liveView = MyViewController()

How to animate custom Progress bar properly (swift)?

I made a custom progressbar for my app (following an article on medium), it works as intended but i have one problem, when i change the progress value then it jumps to fast! (dont get confused by the percent values below the bar, they are off, i know that)
i use setNeedsDisplay() to redraw my view.
I want the bar to animate smoothly, so in my case a bit slower.
this is the draw function of the bar:
override func draw(_ rect: CGRect) {
backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height * 0.25).cgPath
layer.mask = backgroundMask
let progressRect = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
progressLayer.frame = progressRect
progressLayer.backgroundColor = UIColor.black.cgColor
gradientLayer.frame = rect
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
}
Here is the whole Class i used:
https://bitbucket.org/mariwi/custom-animated-progress-bars-with-uibezierpaths/src/master/ProgressBars/Bars/GradientHorizontalProgressBar.swift
Anyone with an idea?
EDIT 1:
Similar questions helped, but the result is not working properly.
I aded this function to set the progress of the bar:
func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds
redrawAnimation.fillMode = .forwards
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = 0.5
progressLayer.bounds = newBounds
gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
progressLayer.add(redrawAnimation, forKey: "redrawAnim")
}
And now the bar behaves like this:
After digging a while and a ton of testing, i came up with a solution, that suited my needs! Altough the above answer from DonMag was also working great (thanks for your effort), i wanted to fix what halfway worked. So the problem was, that the bar resized itself from the middle of the view. And on top, the position was also off for some reason.
First i set the position back to (0,0) so that the view started at the beginning (where it should).
The next thing was the resizing from the middle, because with the position set back, the bar only animated to the half when i set it to 100%. After some tinkering and reading i found out, that changing the anchorPoint of the view would solve my problem. The default value was (0.5,0.5), changing it into (0,0) meant that it would only expand the desired direction.
After that i only needed to re-set the end of the gradient, so that the animation stays consistent between the different values. After all of this my bar worked like I imagined. And here is the result:
Here is the final code, i used to accomplish this:
func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let duration = 0.5
let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds
redrawAnimation.fillMode = .both
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = duration
progressLayer.bounds = newBounds
progressLayer.position = CGPoint(x: 0, y: 0)
progressLayer.anchorPoint = CGPoint(x: 0, y: 0)
progressLayer.add(redrawAnimation, forKey: "redrawAnim")
let oldGradEnd = gradientLayer.endPoint
let newGradEnd = CGPoint(x: progress, y: 0.5)
let gradientEndAnimation = CABasicAnimation(keyPath: "endPoint")
gradientEndAnimation.fromValue = oldGradEnd
gradientEndAnimation.toValue = newGradEnd
gradientEndAnimation.fillMode = .both
gradientEndAnimation.isRemovedOnCompletion = false
gradientEndAnimation.duration = duration
gradientLayer.endPoint = newGradEnd
gradientLayer.add(gradientEndAnimation, forKey: "gradEndAnim")
}
I'm going to suggest a somewhat different approach.
First, instead of adding a sublayer as the gradient layer, we'll make the custom view's layer itself a gradient layer:
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
// then, in init
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
We'll set the gradient animation to the full size of the view... that will give it a consistent width and speed.
Next, we'll add a subview as a mask, instead of a layer-mask. That will allow us to animate its width independently.
class GradProgressView: UIView {
#IBInspectable var color: UIColor = .gray {
didSet { setNeedsDisplay() }
}
#IBInspectable var gradientColor: UIColor = .white {
didSet { setNeedsDisplay() }
}
// this view will mask the percentage width
private let myMaskView = UIView()
// so we can calculate the new-progress-animation duration
private var curProgress: CGFloat = 0.0
public var progress: CGFloat = 0 {
didSet {
// calculate the change in progress
let changePercent = abs(curProgress - progress)
// if the change is 100% (i.e. from 0.0 to 1.0),
// we want the animation to take 1-second
// so, make the animation duration equal to
// 1-second * changePercent
let dur = changePercent * 1.0
// save the new progress
curProgress = progress
// calculate the new width of the mask view
var r = bounds
r.size.width *= progress
// animate the size of the mask-view
UIView.animate(withDuration: TimeInterval(dur), animations: {
self.myMaskView.frame = r
})
}
}
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.locations = [0.25, 0.5, 0.75]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-0.3, -0.15, 0]
animation.toValue = [1, 1.15, 1.3]
animation.duration = 1.5
animation.isRemovedOnCompletion = false
animation.repeatCount = Float.infinity
gradientLayer.add(animation, forKey: nil)
myMaskView.backgroundColor = .white
mask = myMaskView
}
override func layoutSubviews() {
super.layoutSubviews()
// if the mask view frame has not been set at all yet
if myMaskView.frame.height == 0 {
var r = bounds
r.size.width = 0.0
myMaskView.frame = r
}
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
layer.cornerRadius = bounds.height * 0.25
}
}
Here's a sample controller class - each tap will cycle through a list of sample progress percentages:
class ExampleViewController: UIViewController {
let progView = GradProgressView()
let infoLabel = UILabel()
var idx: Int = 0
let testVals: [CGFloat] = [
0.75, 0.3, 0.95, 0.25, 0.5, 1.0,
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
[infoLabel, progView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
infoLabel.textColor = .white
infoLabel.textAlignment = .center
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
progView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
progView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
progView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
progView.heightAnchor.constraint(equalToConstant: 40.0),
infoLabel.topAnchor.constraint(equalTo: progView.bottomAnchor, constant: 8.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
progView.color = #colorLiteral(red: 0.9932278991, green: 0.5762576461, blue: 0.03188031539, alpha: 1)
progView.gradientColor = #colorLiteral(red: 1, green: 0.8578521609, blue: 0.3033572137, alpha: 1)
// add a tap gesture recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))
view.addGestureRecognizer(t)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didTap(nil)
}
#objc func didTap(_ g: UITapGestureRecognizer?) -> Void {
let n = idx % testVals.count
progView.progress = testVals[n]
idx += 1
infoLabel.text = "Auslastung \(Int(testVals[n] * 100))%"
}
}

How to create a rotating rainbow color circle in iOS

From stackoverflow i got a code for drawing rainbow color circle.But as part of requirement ,I need that circle to be rotated continously ,like a rotating progress loader.Below is the code used for creating Rainbow color circle.
class RainbowCircle: UIView {
private var radius: CGFloat {
return frame.width>frame.height ? frame.height/2 : frame.width/2
}
private var stroke: CGFloat = 10
private var padding: CGFloat = 5
//MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
drawRainbowCircle(outerRadius: radius - padding, innerRadius: radius - stroke - padding, resolution: 1)
}
init(frame: CGRect, lineHeight: CGFloat) {
super.init(frame: frame)
stroke = lineHeight
}
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
/*
Resolution should be between 0.1 and 1
*/
private func drawRainbowCircle(outerRadius: CGFloat, innerRadius: CGFloat, resolution: Float) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.translateBy(x: self.bounds.midX, y: self.bounds.midY) //Move context to center
let subdivisions:CGFloat = CGFloat(resolution * 512) //Max subdivisions of 512
let innerHeight = (CGFloat.pi*innerRadius)/subdivisions //height of the inner wall for each segment
let outterHeight = (CGFloat.pi*outerRadius)/subdivisions
let segment = UIBezierPath()
segment.move(to: CGPoint(x: innerRadius, y: -innerHeight/2))
segment.addLine(to: CGPoint(x: innerRadius, y: innerHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: outterHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: -outterHeight/2))
segment.close()
//Draw each segment and rotate around the center
for i in 0 ..< Int(ceil(subdivisions)) {
UIColor(hue: CGFloat(i)/subdivisions, saturation: 1, brightness: 1, alpha: 1).set()
segment.fill()
//let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions //The amount of space between the tails of each segment
let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions
segment.lineWidth = lineTailSpace //allows for seemless scaling
segment.stroke()
// //Rotate to correct location
let rotate = CGAffineTransform(rotationAngle: -(CGFloat.pi*2/subdivisions)) //rotates each segment
segment.apply(rotate)
}
Please anyone help me in rotating this circle.
Please find below the circle generated with above code:
What you got looks completely overcomplicated in the first place. Take a look at the following example:
class ViewController: UIViewController {
class RainbowView: UIView {
var segmentCount: Int = 10 {
didSet {
refresh()
}
}
var lineWidth: CGFloat = 10 {
didSet {
refresh()
}
}
override var frame: CGRect {
didSet {
refresh()
}
}
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
private var currentGradientLayer: CAGradientLayer?
private func refresh() {
currentGradientLayer?.removeFromSuperlayer()
guard segmentCount > 0 else { return }
currentGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.type = .conic
let colors: [UIColor] = {
var colors: [UIColor] = [UIColor]()
for i in 0..<segmentCount {
colors.append(UIColor(hue: CGFloat(i)/CGFloat(segmentCount), saturation: 1, brightness: 1, alpha: 1))
}
colors.append(UIColor(hue: 0.0, saturation: 1, brightness: 1, alpha: 1)) // Append start color at the end as well to complete the circle
return colors;
}()
gradientLayer.colors = colors.map { $0.cgColor }
gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
gradientLayer.mask = {
let shapeLayer = CAShapeLayer()
shapeLayer.frame = bounds
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.path = UIBezierPath(ovalIn: bounds.inset(by: UIEdgeInsets(top: lineWidth*0.5, left: lineWidth*0.5, bottom: lineWidth*0.5, right: lineWidth*0.5))).cgPath
return shapeLayer
}()
return gradientLayer
}()
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview({
let view = RainbowView(frame: CGRect(x: 50.0, y: 100.0, width: 100.0, height: 100.0))
var angle: CGFloat = 0.0
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true, block: { _ in
angle += 0.01
view.transform = CGAffineTransform(rotationAngle: angle)
})
return view
}())
}
}
So a view is generated that uses a conical gradient with mask to draw the circle you are describing. Then a transform is applied to the view to rotate it. And a Timer is scheduled to rotate the circle.
Note that this code will leak because timer is nowhere invalidated. It needs to be removed when view disappears or similar.
The easiest way would be to attach an animation that repeats forever:
let animation = CABasicAnimation(keyPath: "transform.rotation") // Create rotation animation
animation.repeatCount = .greatestFiniteMagnitude // Repeat animation for as long as we can
animation.fromValue = 0 // Rotate from 0
animation.toValue = 2 * Float.pi // to 360 deg
animation.duration = 1 // During 1 second
self.layer.add(animation, forKey: "animation") // Adding the animation to the view
self - is RainbowCircle, assuming that you add this code to one of the methods inside it.
For this we can have Image something like this
syncImage.image = UIImage(named:"spinning")
Create a below extension to Start/Stop Rotating
extension UIView {
// To animate
func startRotating(duration: Double = 1) {
let kAnimationKey = "rotation"
if self.layer.animationForKey(kAnimationKey) == nil {
let animate = CABasicAnimation(keyPath: "transform.rotation")
animate.duration = duration
animate.repeatCount = Float.infinity
animate.fromValue = 0.0
animate.toValue = Float(M_PI * 2.0)
self.layer.addAnimation(animate, forKey: kAnimationKey)
}
}
func stopRotating() {
let kAnimationKey = "rotation"
if self.layer.animationForKey(kAnimationKey) != nil {
self.layer.removeAnimationForKey(kAnimationKey)
}
}
}
Usage
func startSpinning() {
syncImage.startRotating()
}
func stopSpinning() {
syncImage.stopRotating()
}
func handleSyncTap(sender: UITapGestureRecognizer? = nil) {
startSpinning()
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(3 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.stopSpinning()
})
}

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

Prevent CAGradientLayer locations from returning to original values after using CABasicAnimation

I'm trying to make a gradient animate so that the locations of the two colors shift from left to right. The problems is that once the animation completes, the gradient locations snap back to their original position.
Here is my playground code:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
let gradientLayer: CAGradientLayer = {
let layer = CAGradientLayer()
layer.colors = [ UIColor.red.cgColor, UIColor.blue.cgColor ]
layer.locations = [0.0, 0.5]
layer.startPoint = CGPoint(x: 0.0, y: 1.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
return layer
}()
override func viewDidLoad() {
super.viewDidLoad()
view.layer.addSublayer(gradientLayer)
gradientLayer.frame = view.bounds
let gradientChangeAnimation = CABasicAnimation(keyPath: "locations")
gradientChangeAnimation.duration = 1
gradientChangeAnimation.toValue = [0.5, 1.0]
gradientLayer.add(gradientChangeAnimation, forKey: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
gradientLayer.frame = view.frame
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
How do I prevent the reset of the locations after the animation completes?
You can just tell it to not remove on completion and set the locations to the new values:
override func viewDidLoad() {
super.viewDidLoad()
view.layer.addSublayer(gradientLayer)
gradientLayer.frame = view.bounds
CATransaction.begin()
CATransaction. setCompletionBlock {
gradientLayer.locations = [0.5, 1.0]
gradientLayer.removeAllAnimations()
}
let gradientChangeAnimation = CABasicAnimation(keyPath: "locations")
gradientChangeAnimation.duration = 1
gradientChangeAnimation.toValue = [0.5, 1.0]
gradientLayer.add(gradientChangeAnimation, forKey: nil)
CATransaction.commit()
}
or set the fillMode and isRemovedOnCompletion properties appropriately.
override func viewDidLoad() {
super.viewDidLoad()
view.layer.addSublayer(gradientLayer)
gradientLayer.frame = view.bounds
let gradientChangeAnimation = CABasicAnimation(keyPath: "locations")
gradientChangeAnimation.duration = 1
gradientChangeAnimation.toValue = [0.5, 1.0]
gradientChangeAnimation.fillMode = kCAFillModeForwards
gradientChangeAnimation.isRemovedOnCompletion = false
gradientLayer.add(gradientChangeAnimation, forKey: nil)
}

Resources