CALayer Presentation nil - uiview

I am trying to use a CABasicAnimation for the timing function with custom objects (not UIView).
I'm trying to implement #CIFilter's answer from here which is to use the CALayer's presentation layer that is animated to evaluate the timing function.
I'm doing it all in viewDidAppear, so a valid view exists, but no matter what I do, the Presentation layer is always nil.
Note that I have to add the animation to the view's layer and not the layer I've added to it for it to animate at all. And if I uncomment the lines commented out below I can see that the animation works (but only when animating the root layer). Regardless, the Presentation layer is nil.
I've looked at dozen's of tutorials and SO answers, and it seems this should just work, so I suppose I must be doing something stupid.
I am just trying to use the CoreAnimation timing functions. I have UICubicTimingParameters working, but seems like going the CA route offers much more functionality which would be nice.
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let newView = UIView(frame: view.frame)
view.addSubview(newView)
let evaluatorLayer = CALayer()
evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
evaluatorLayer.borderWidth = 8.0
evaluatorLayer.borderColor = UIColor.purple.cgColor
evaluatorLayer.timeOffset = 0.3
evaluatorLayer.isHidden = true
// evaluatorLayer.isHidden = false
newView.layer.addSublayer(evaluatorLayer)
let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
basicAnimation.duration = 1.0
basicAnimation.fromValue = 0.0
basicAnimation.toValue = 100.0
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.speed = 0.0
// basicAnimation.speed = 0.1
newView.layer.add(basicAnimation, forKey: "evaluate")
if let presentationLayer = newView.layer.presentation() {
let evaluatedValue = presentationLayer.bounds.origin.x
print("evaluatedValue: \(evaluatedValue)")
}
else {
print(evaluatorLayer.presentation())
}
}
}

Not sure if your code is going to do what you expect, but...
I think the reason .presentation() is nil is because you haven't given UIKit an opportunity to apply the animation.
Try this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let newView = UIView(frame: view.frame)
view.addSubview(newView)
let evaluatorLayer = CALayer()
evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
evaluatorLayer.borderWidth = 8.0
evaluatorLayer.borderColor = UIColor.purple.cgColor
evaluatorLayer.timeOffset = 0.3
evaluatorLayer.isHidden = true
// evaluatorLayer.isHidden = false
newView.layer.addSublayer(evaluatorLayer)
let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
basicAnimation.duration = 1.0
basicAnimation.fromValue = 0.0
basicAnimation.toValue = 100.0
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.speed = 0.0
// basicAnimation.speed = 0.1
newView.layer.add(basicAnimation, forKey: "evaluate")
DispatchQueue.main.async {
if let presentationLayer = newView.layer.presentation() {
let evaluatedValue = presentationLayer.bounds.origin.x
print("async evaluatedValue: \(evaluatedValue)")
}
else {
print("async", evaluatorLayer.presentation())
}
}
if let presentationLayer = newView.layer.presentation() {
let evaluatedValue = presentationLayer.bounds.origin.x
print("immediate evaluatedValue: \(evaluatedValue)")
}
else {
print("immediate", evaluatorLayer.presentation())
}
}
My debug output is:
immediate nil
async evaluatedValue: 0.0
Edit
I'm still not sure what your goal is, but give this a try...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let newView = UIView(frame: view.frame)
view.addSubview(newView)
let evaluatorLayer = CALayer()
evaluatorLayer.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
evaluatorLayer.borderWidth = 8.0
evaluatorLayer.borderColor = UIColor.purple.cgColor
evaluatorLayer.isHidden = true
//evaluatorLayer.isHidden = false
newView.layer.addSublayer(evaluatorLayer)
let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
basicAnimation.duration = 1.0
basicAnimation.fromValue = 0.0
basicAnimation.toValue = 100.0
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.speed = 0.0
//basicAnimation.speed = 1.0
// set timeOffset on the animation, not on the layer itself
basicAnimation.timeOffset = 0.3
// add animation to evaluatorLayer
evaluatorLayer.add(basicAnimation, forKey: "evaluate")
DispatchQueue.main.async {
// get presentation layer of evaluatorLayer
if let presentationLayer = evaluatorLayer.presentation() {
let evaluatedValue = presentationLayer.bounds.origin.x
print("async evaluatedValue: \(evaluatedValue)")
}
else {
print("async", evaluatorLayer.presentation())
}
}
}
In this example, we apply the .timeOffset on the animation, not on the layer. And, we add the animation to the evaluatorLayer, not to the newView.layer.
Output (for my quick test):
async evaluatedValue: 30.000001192092896

Related

How do I delete CABasicAnimation and then restart it in Swift?

I am building an app that will show an animation when a certain function is running and remove it once the next function runs. However I need it to restart when it loops back.
I have been able to remove it from the view with
isHidden = true
But if I rely on this to bring it back it layers a second animation over the first when it runs again and I unhide it.
I was also able to use .removeFromSuperView()
to make it disappear but I can't figure out how to bring it back after that.
Here is the relevant code:
class ViewController: UIViewController {
// some code
let dots = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// some code
dots.translatesAutoresizingMaskIntoConstraints = false
dots.backgroundColor = .red
dots.center = self.view.center
view.addSubview(dots)
// Some Auto Layout code
}
func showAnimatingDotsInImageView(_ isOn: Bool) {
let lay = CAReplicatorLayer()
if isOn == true {
lay.frame = CGRect(x: -55, y: 0, width: 30, height: 14) //yPos == 12
let circle = CALayer()
circle.frame = CGRect(x: 10, y: 10, width: 10, height: 10)
circle.cornerRadius = circle.frame.width / 2
circle.backgroundColor = rocketDark.cgColor//lightGray.cgColor //UIColor.black.cgColor
lay.addSublayer(circle)
lay.instanceCount = 5
lay.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
anim.fromValue = 0.0
anim.toValue = 1.0
anim.duration = 2
anim.repeatCount = .infinity
circle.add(anim, forKey: "animation")
lay.instanceDelay = anim.duration / Double(lay.instanceCount)
dots.layer.addSublayer(lay)
} else if isOn == false {
//lay.removeFromSuperlayer()
//lay.removeAllAnimations()
dots.removeFromSuperview()
//dots.stopAnimating()
}
}
As you can probably tell I was attempting to hack together a way to write howAnimatingDotsInImageView(false) to remove it and howAnimatingDotsInImageView(true) to restart it.
I didn't compile this code, but I think using this approach you can remove your animation
class ViewController: UIViewController {
// some code
let dots = UIImageView()
let circleLayer: CAlayer? // Save circle layer property
override func viewDidLoad() {
super.viewDidLoad()
// some code
dots.translatesAutoresizingMaskIntoConstraints = false
dots.backgroundColor = .red
dots.center = self.view.center
view.addSubview(dots)
// Some Auto Layout code
}
func showAnimatingDotsInImageView(_ isOn: Bool) {
let lay = CAReplicatorLayer()
if isOn == true {
lay.frame = CGRect(x: -55, y: 0, width: 30, height: 14) //yPos == 12
circleLayer = CALayer()
circleLayer?.frame = CGRect(x: 10, y: 10, width: 10, height: 10)
circleLayer?.cornerRadius = circle?.frame.width / 2 ?? .zero
circleLayer?.backgroundColor = rocketDark.cgColor//lightGray.cgColor //UIColor.black.cgColor
circleLayer.map { lay.addSublayer($0) }
lay.instanceCount = 5
lay.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
anim.fromValue = 0.0
anim.toValue = 1.0
anim.duration = 2
anim.repeatCount = .infinity
circleLayer?.add(anim, forKey: "animation")
lay.instanceDelay = anim.duration / Double(lay.instanceCount)
dots.layer.addSublayer(lay)
} else if isOn == false {
circleLayer?.removeAnimation(forKey: "animation")
}
}

Is the a way to have a timing function work on multiple animations in Swift?

I'm trying to recreate the Activity ring that Apple uses in their activity apps. Here is an image for those unaware.
Apple Progress Ring.
I've done a decent job of recreating it, something I especially struggled with was the overlapping shadow. In the end, the workaround I used was to split the ring into two parts, the first 75% and the last 25% so I could have the last 25% have a shadow and appear to overlap with itself.
Now that I have done this, the animation timing has become more difficult. I now have three animations that I need to take care of.
The first 75% of the ring
The last 25% of the ring
Rotating the ring if it surpasses 100%
Here is a video demonstrating this. Streamable Link.
For illustrative purposes, here is the last 25% coloured differently so you can visualise it.
As you can see, the timing of the animation is a bit janky. So I have my timings set as follows
If the ring is filled at 75% or less, it takes 2.25 seconds to fill with a timing function of ease out
If the ring is filled between 75 and 100%, the first 75% takes 1 second to fill and the last 25% takes 1.25 seconds to fill with a timing function of ease out.
If the ring is filled over 100%, the first 75% takes 1 second, the last 25% takes 1 second and the rotation for the ring also takes 1 second with a timing function of ease out.
My question is, is it possible to link these seperate CABasicAnimations so I can set a total time of 2.25 seconds as well as set a timing function for that group so timing is calculated dynamically for each animation and a timing function affects all three?
Here is my code so far, it consists of 3 animation functions.
percent = How much to fill the ring
gradientMaskPart1 = first 75% ring layer
gradientMaskPart2 = last 25% ring layer
containerLayer = layer that holds both gradientMaskParts and is rotated to simute the ring overlapping itself.
private func animateRing() {
let needsMultipleAnimations = percent <= 0.75 ? false : true
CATransaction.begin()
if needsMultipleAnimations { CATransaction.setCompletionBlock(ringEndAnimation) }
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.fromValue = currentFill
basicAnimation.toValue = percent > 0.75 ? 0.75 : percent
currentFill = Double(percent)
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.duration = needsMultipleAnimations ? 1 : 2.25
basicAnimation.timingFunction =
needsMultipleAnimations ? .none : CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
gradientMaskPart1.add(basicAnimation, forKey: "basicAnimation")
CATransaction.commit()
}
private func ringEndAnimation() {
let needsMultipleAnimations = percent <= 1 ? false : true
CATransaction.begin()
if needsMultipleAnimations { CATransaction.setCompletionBlock(rotateRingAnimation) }
let duration = needsMultipleAnimations ? 1 : 1.25
let timingFunction: CAMediaTimingFunction? =
needsMultipleAnimations ? .none : CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.fromValue = 0
basicAnimation.toValue = percent <= 1 ? (percent-0.75)*4 : 1
basicAnimation.duration = duration
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.timingFunction = timingFunction
self.gradientMaskPart2.isHidden = false
self.gradientMaskPart2.add(basicAnimation2, forKey: "basicAnimation")
CATransaction.commit()
}
private func rotateRingAnimation() {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.fromValue = 2*CGFloat.pi
rotationAnimation.toValue = 2*CGFloat.pi+((2*(percent-1)*CGFloat.pi))
rotationAnimation.duration = 1
rotationAnimation.fillMode = CAMediaTimingFillMode.forwards
rotationAnimation.isRemovedOnCompletion = false
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
self.containerLayer.add(rotationAnimation, forKey: "rotation")
}
This solution demonstrates how to animate several layers synchronously with their dedicated CABasicAnimation by using CAAnimationGroup.
Create a container CALayer subclass for layers that should be animated synchronously. Let's call it TestLayer
Define the property for every layer you need to animate.
Override needsDisplay(forKey:) in the TestLayer to receive the display call when animation modifies the particular layer via keyPath.
Override init(layer:) in the TestLayer to assign your sublayers to the testLayer.presentation() layer that renders the animation of the TestLayer.
Here is the whole code with TestView that hosts TestLayer.
class TestLayer: CALayer {
#NSManaged #objc dynamic var layer1: CAGradientLayer
#NSManaged #objc dynamic var layer2: CAGradientLayer
override init() {
super.init()
let gradientLayer1 = CAGradientLayer()
let gradientLayer2 = CAGradientLayer()
gradientLayer1.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
gradientLayer2.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
gradientLayer1.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer1.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer2.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer2.endPoint = CGPoint(x: 0.5, y: 0.5)
addSublayer(gradientLayer1)
addSublayer(gradientLayer2)
layer1 = gradientLayer1
layer2 = gradientLayer2
}
override init(layer: Any) {
super.init(layer: layer)
if let testLayer = layer as? TestLayer {
layer1 = testLayer.layer1
layer2 = testLayer.layer2
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSublayers() {
var bounds1 = self.bounds
bounds1.size.height /= 2.0
var bounds2 = bounds1
bounds2.origin.y = bounds1.maxY
super.layoutSublayers()
layer1.frame = bounds1
layer2.frame = bounds2
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == "layer1" || key == "layer2" {
return true
}
return super.needsDisplay(forKey: key)
}
}
--
class TestView: UIView {
override class var layerClass: AnyClass {
get {
return TestLayer.self
}
}
var testLayer: TestLayer {
get {
return layer as! TestLayer
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func startAnimation() {
let animLayer1 = CABasicAnimation(keyPath: "layer1.startPoint")
animLayer1.fromValue = CGPoint(x: 0.5, y: 0.5)
animLayer1.toValue = CGPoint(x: 0.0, y: 0.5)
animLayer1.duration = 1.0
animLayer1.autoreverses = true
let animLayer2 = CABasicAnimation(keyPath: "layer2.endPoint")
animLayer2.fromValue = CGPoint(x: 0.5, y: 0.5)
animLayer2.toValue = CGPoint(x: 1.0, y: 0.5)
animLayer2.duration = 1.0
animLayer2.autoreverses = true
let group = CAAnimationGroup()
group.duration = 1.0
group.autoreverses = true
group.repeatCount = Float.greatestFiniteMagnitude
group.animations = [animLayer1, animLayer2]
testLayer.add(group, forKey: "TestAnim")
}
}
If you want to test it, create a dummy view controller, and override its viewWillAppear like this:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let testView = TestView()
testView.frame = view.bounds
testView.translatesAutoresizingMaskIntoConstraints = true
testView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(testView)
testView.startAnimation()
}

inner shadow effect UIView

I want to add inner shadow to this UIlabel
Picture:
reference form Inner shadow effect on UIView layer?
let display = UILabel()
func setBackgroundLayer () {
let backgroundLayer = CAGradientLayer()
backgroundLayer.frame = view.bounds
backgroundLayer.colors = [UIColor(white:1,alpha:0.5).cgColor,UIColor(white:0.5,alpha:0.5).cgColor]
view.layer.insertSublayer(backgroundLayer, at: 0)
}
func setDisplay() {
let Xratio = view.bounds.width/8
display.frame = CGRect(x:Xratio,y:80,width:Xratio*6,height:Xratio*2)
display.backgroundColor = UIColor(red:204/255,green:203/255,blue:181/255,alpha:1)
view.addSubview(display)
}
func displayInnerShadow() {
let shadowLayer = CAShapeLayer()
shadowLayer.frame = view.bounds
let path = CGMutablePath()
path.addRect(display.frame.insetBy(dx: -20, dy: -20))
path.addRect(display.frame)
shadowLayer.fillRule = kCAFillRuleEvenOdd
shadowLayer.path = path
shadowLayer.shadowColor = UIColor(white:0,alpha:1).cgColor
shadowLayer.shadowOffset = CGSize(width:0,height:0)
shadowLayer.shadowOpacity = 1
shadowLayer.shadowRadius = 5
view.layer.insertSublayer(shadowLayer, at: 1)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setBackgroundLayer()
setDisplay()
//displayInnerShadow()
}
I got this result:
Why isn't it working and how to set the fill color?
try this
let display = UILabel()
func setBackgroundLayer () {
let backgroundLayer = CAGradientLayer()
backgroundLayer.frame = view.bounds
backgroundLayer.colors = [UIColor(white:1,alpha:0.5).cgColor,UIColor(white:0.5,alpha:0.5).cgColor]
view.layer.insertSublayer(backgroundLayer, at: 0)
}
func setDisplay() {
let Xratio = view.bounds.width/8
display.frame = CGRect(x:Xratio,y:80,width:Xratio*6,height:Xratio*2)
set layer background color instead of display.backgroundColor
display.layer.backgroundColor = UIColor(red:204/255,green:203/255,blue:181/255,alpha:1).cgColor
view.addSubview(display)
}
func displayInnerShadow() {
let innerShadowLayer = CALayer()
innerShadowLayer.frame = display.bounds
let path = UIBezierPath(rect: innerShadowLayer.bounds.insetBy(dx: -20, dy: -20))
let innerPart = UIBezierPath(rect: innerShadowLayer.bounds).reversing()
path.append(innerPart)
innerShadowLayer.shadowPath = path.cgPath
innerShadowLayer.masksToBounds = true
innerShadowLayer.shadowColor = UIColor.black.cgColor
innerShadowLayer.shadowOffset = CGSize.zero
innerShadowLayer.shadowOpacity = 1
innerShadowLayer.shadowRadius = 5
display.layer.addSublayer(innerShadowLayer)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setDisplay()
setBackgroundLayer()
displayInnerShadow()
}

Core Animation - modify animation property

I have animation
func startRotate360() {
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = 1
rotation.isCumulative = true
rotation.repeatCount = Float.greatestFiniteMagnitude
self.layer.add(rotation, forKey: "rotationAnimation")
}
What I want is ability to stop animation by setting its repeat count to 1, so it completes current rotation (simply remove animation is not ok because it looks not good)
I try following
func stopRotate360() {
self.layer.animation(forKey: "rotationAnimation")?.repeatCount = 1
}
But I get crash and in console
attempting to modify read-only animation
How to access writable properties ?
Give this a go. You can in fact change CAAnimations that are in progress. There are so many ways. This is the fastest/simplest. You could even stop the animation completely and resume it without the user even noticing.
You can see the start animation function along with the stop. The start animation looks similar to yours while the stop grabs the current rotation from the presentation layer and creates an animation to rotate until complete. I also smoothed out the duration to be a percentage of the time needed to complete based on current rotation z to full rotation based on the running animation. Then I remove the animation with the repeat count and add the new animation. You can see the view rotate smoothly to the final position and stop. You will get the idea. Drop it in and run it and see what you think. Hit the button to start and hit it again to see it finish rotation and stop.
import UIKit
class ViewController: UIViewController {
var animationView = UIView()
var button = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
animationView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
animationView.backgroundColor = .green
animationView.center = view.center
self.view.addSubview(animationView)
let label = UILabel(frame: animationView.bounds)
label.text = "I Spin"
animationView.addSubview(label)
button = UIButton(frame: CGRect(x: 20, y: animationView.frame.maxY + 60, width: view.bounds.width - 40, height: 40))
button.setTitle("Animate", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(ViewController.pressed), for: .touchUpInside)
self.view.addSubview(button)
}
func pressed(){
if let title = button.titleLabel?.text{
let trans = CATransition()
trans.type = "rippleEffect"
trans.duration = 0.6
button.layer.add(trans, forKey: nil)
switch title {
case "Animate":
//perform animation
button.setTitle("Stop And Finish", for: .normal)
rotateAnimationRepeat()
break
default:
//stop and finish
button.setTitle("Animate", for: .normal)
stopAnimationAndFinish()
break
}
}
}
func rotateAnimationRepeat(){
//just to be sure because of how i did the project
animationView.layer.removeAllAnimations()
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = 0.5
rotation.repeatCount = Float.greatestFiniteMagnitude
//not doing cumlative
animationView.layer.add(rotation, forKey: "rotationAnimation")
}
func stopAnimationAndFinish(){
if let presentation = animationView.layer.presentation(){
if let currentRotation = presentation.value(forKeyPath: "transform.rotation.z") as? CGFloat{
var duration = 0.5
//smooth out duration for change
duration = Double((CGFloat(Double.pi * 2) - currentRotation))/(Double.pi * 2)
animationView.layer.removeAllAnimations()
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = currentRotation
rotation.toValue = Double.pi * 2
rotation.duration = duration * 0.5
animationView.layer.add(rotation, forKey: "rotationAnimation")
}
}
}
}
Result:
2019 typical modern syntax
Setup the arc and the layer like this:
import Foundation
import UIKit
class RoundChaser: UIView {
private let lineThick: CGFloat = 10.0
private let beginFraction: CGFloat = 0.15
// where does the arc drawing begin?
// 0==top, .25==right, .5==bottom, .75==left
private lazy var arcPath: CGPath = {
let b = beginFraction * .pi * 2.0
return UIBezierPath(
arcCenter: bounds.centerOfCGRect(),
radius: bounds.width / 2.0 - lineThick / 2.0,
startAngle: .pi * -0.5 + b,
// recall that .pi * -0.5 is the "top"
endAngle: .pi * 1.5 + b,
clockwise: true
).cgPath
}()
private lazy var arcLayer: CAShapeLayer = {
let l = CAShapeLayer()
l.path = arcPath
l.fillColor = UIColor.clear.cgColor
l.strokeColor = UIColor.purple.cgColor
l.lineWidth = lineThick
l.lineCap = CAShapeLayerLineCap.round
l.strokeStart = 0
l.strokeEnd = 0
// if both are same, it is hidden. initially hidden
layer.addSublayer(l)
return l
}()
then initialization is this easy
open override func layoutSubviews() {
super.layoutSubviews()
arcLayer.frame = bounds
}
finally animation is easy
public func begin() {
CATransaction.begin()
let e : CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
e.duration = 2.0
e.fromValue = 0
e.toValue = 1.0
// recall 0 will be our beginFraction, see above
e.repeatCount = .greatestFiniteMagnitude
self.arcLayer.add(e, forKey: nil)
CATransaction.commit()
}
}
Maybe this is not the best solution but it works, as you say you can not modify properties of the CABasicAnimation once is created, also we need to remove the rotation.repeatCount = Float.greatestFiniteMagnitude, if notCAAnimationDelegatemethodanimationDidStop` is never called, with this approach the animation can be stoped without any problems as you need
step 1: first declare a variable flag to mark as you need stop animation in your custom class
var needStop : Bool = false
step 2: add a method to stopAnimation after ends
func stopAnimation()
{
self.needStop = true
}
step 3: add a method to get your custom animation
func getRotate360Animation() ->CAAnimation{
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = 1
rotation.isCumulative = true
rotation.isRemovedOnCompletion = false
return rotation
}
step 4: Modify your startRotate360 func to use your getRotate360Animation() method
func startRotate360() {
let rotationAnimation = self.getRotate360Animation()
rotationAnimation.delegate = self
self.layer.add(rotationAnimation, forKey: "rotationAnimation")
}
step 5: Implement CAAnimationDelegate in your class
extension YOURCLASS : CAAnimationDelegate
{
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if(anim == self.layer?.animation(forKey: "rotationAnimation"))
{
self.layer?.removeAnimation(forKey: "rotationAnimation")
if(!self.needStop){
let animation = self.getRotate360Animation()
animation.delegate = self
self.layer?.add(animation, forKey: "rotationAnimation")
}
}
}
}
This works and was tested
Hope this helps you

Fade in/out animation for a series of dots

I have an upload UIButton and I'm trying to implement an animation where I have a group of individual dots that line up horizontally underneath the button and have the very left dot fade in/out, then have the dot next to it fade in/out, and so on all the way to the right most dot, then start the cycle over again from the left. I can get this working for one dot, but what would be the most efficient way to do this for multiple dots?
func dotAnimation(){
let xCoord = self.recButt.frame.origin.x + 25
let yCoord = self.recButt.frame.origin.y + 5
let radius = 3
let dotPath = UIBezierPath(ovalIn: CGRect(x: Int(xCoord), y: Int(yCoord), width: radius, height: radius))
let layer = CAShapeLayer()
layer.path = dotPath.cgPath
layer.strokeColor = UIColor(red: 95.00/255, green: 106.00/255, blue: 255/255, alpha: 1.00).cgColor
self.view.layer.addSublayer(layer)
let animation : CABasicAnimation = CABasicAnimation(keyPath: "opacity");
animation.autoreverses = true
animation.fromValue = 0
animation.toValue = 1
animation.duration = 2.0
layer.add(animation, forKey: nil)
}
You are describing a CAReplicatorLayer. Here is one that does the sort of thing you're after, using bars instead of dots:
Here's the code for that. Note that there is only one red bar layer; the replication into five, and the offset fading animation, is handled by the containing replicator layer:
let lay = CAReplicatorLayer()
lay.frame = CGRect(0,0,100,20)
let bar = CALayer()
bar.frame = CGRect(0,0,10,20)
bar.backgroundColor = UIColor.red.cgColor
lay.addSublayer(bar)
lay.instanceCount = 5
lay.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
anim.fromValue = 1.0
anim.toValue = 0.2
anim.duration = 1
anim.repeatCount = .infinity
bar.add(anim, forKey: nil)
lay.instanceDelay = anim.duration / Double(lay.instanceCount)
// and now just add `lay` to the interface
Here is another version of animating three dots in any UILabel
private var displayLink: CADisplayLink?
private var loadingLabeltext: String = ""
private var loadingLabel: UILabel? = {
let label = UILabel()
label.textColor = UIColor.appRed
label.textAlignment = .center
label.font = UIFont.init(name: "CirceRounded-Bold", size: 15)
label.minimumScaleFactor = 0.6
return label
}()
override func viewDidLoad(_ animated: Bool) {
super.viewDidLoad(animated)
view.addSubview(loadingLabel!)
loadingLabel!.text = "Loading ..."
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animateLabelDots(label: loadingLabel!)
}
private func animateLabelDots(label: UILabel) {
guard var text = label.text else { return }
text = String(text.dropLast(3))
loadingLabeltext = text
displayLink = CADisplayLink(target: self, selector: #selector(showHideDots))
displayLink?.add(to: .main, forMode: .common)
displayLink?.preferredFramesPerSecond = 4
}
#objc private func showHideDots() {
if !loadingLabeltext.contains("...") {
loadingLabeltext = loadingLabeltext.appending(".")
} else {
loadingLabeltext = "Loading "
}
loadingLabel!.text = loadingLabeltext
}
And when you want to stop it just invalidate and deinitizlize display link like this :
private func hideLoadingSpinner() {
displayLink?.invalidate()
displayLink = nil
UIView.animate(withDuration: 0.5, animations: {
loadingLabel?.text = "Finished!"
loadingLabel?.alpha = 0
})
}

Resources