Resuming drawing a CAShapeLayer when view reenters foreground? - ios

func makeACircle(circle: UIView, stokeStart: Double, duration: Double){
var progressCircle = CAShapeLayer();
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2);
let circleRadius : CGFloat = circle.bounds.width / 2
var circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true );
progressCircle = CAShapeLayer ();
progressCircle.path = circlePath.cgPath;
progressCircle.strokeColor = UIColor.white.cgColor;
progressCircle.fillColor = UIColor.clear.cgColor;
progressCircle.lineWidth = 10;
progressCircle.strokeStart = 0;
progressCircle.strokeEnd = 1;
progressCircle.borderWidth = 1
progressCircle.borderColor = UIColor.black.cgColor
circle.layer.addSublayer(progressCircle);
// progressCircle.clipsToBounds = false
self.view.addSubview(circle)
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = stokeStart
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
progressCircle.strokeEnd = 1.0
// Do the actual animation
progressCircle.add(animation, forKey: "animateCircle")
}
I'm drawing circles in with the above code. It draws a circle around buttons on the screen that represent time that's passed since the creation of the button. This code works, but when I go to the homescreen and come back, all of the circles on the screen are completely filled in regardless of how much time was left. If within the app I then switch to another page and then come back, it fixes it.
I'm calling makeACircle from within this firebase query
currentUserRef?.child("invitedToPosts").queryOrdered(byChild: "timestamp").queryStarting(atValue: cutoff).observe(.childAdded) { (snapshot: FIRDataSnapshot) in
Once I have enough information about a button to be made, I make the button, then call the makeACircle.
Any ideas on how to prevent the circles from not appearing as if they've reached the strokeEnd when I load in from the homepage?

When you pause the animation,
Look at the presentation layer and save its strokeEnd.
You need to stop the animation, do so. You may want to set the strokeEnd from the presentation layer's value to avoid any jarring changes.
See how much time has elapsed since you started the animation (e.g. compare CACurrentMediaTime() when you paused vs the value when you started the animation) in order to figure out how much time is remaining in the animation.
Then when you re-present the layer, you now have the fromValue for the strokeEnd and what the remaining duration is.
Another thing that can be useful is to set the delegate of the animation and then use animationDidStop to nil your "in progress" state variables if the animation successfully finishes (i.e. if finished is true).

Related

CABasicAnimation Doesn't End at toValue

I have a circular CAShapeLayer where I'd like to on tap, change the stroke and fill colors. To do so, I added a CABasicAnimation, but to my confusion, the animation doesn't end to the set toValue. Instead, the animation, resets back to the original color.
I tried using the animationDidStop delegate method in setting the stroke and fill colors to the desired end color, which worked, but a flashing glitch of the original colors appeared.
Any guidance would be appreciated.
func createCircleShapeLayer() {
let layer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: view.center, radius: 50, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
layer.path = circularPath.cgPath
layer.strokeColor = UIColor.red.cgColor
layer.lineWidth = 10
layer.fillColor = UIColor.clear.cgColor
layer.lineCap = CAShapeLayerLineCap.round
layer.frame = view.bounds
view.layer.addSublayer(layer)
}
func animateLayerOnTap() {
let strokeAnim = CABasicAnimation(keyPath: "strokeColor")
strokeAnim.toValue = UIColor.red.cgColor
strokeAnim.duration = 0.8
strokeAnim.repeatCount = 0
strokeAnim.autoreverses = false
let fillAnim = CABasicAnimation(keyPath: "fillColor")
fillAnim.toValue = UIColor.lightGray.cgColor
fillAnim.duration = 0.8
fillAnim.repeatCount = 0
fillAnim.autoreverses = false
layer.add(fillAnim, forKey: "fillColor")
layer.add(strokeAnim, forKey: "strokeColor")
}
UIView animation is pretty clean and simple to use. When you submit a block-based UIView animation, the system creates an animation that animates your view's properties to their end values, and they stay there. (Under the covers UIView animation adds one or more CAAnimations to the view's layer, and manages updating the view's properties to the final values once the animation is complete, but you don't need to care about those details.)
None of that is true with Core Animation. It's confusing, non-intuitive, and poorly documented. With Core animation, the animation adds a temporary "presentation layer" on top of your layer that creates the illusion that your properties are animating from their start to the their end state, but it is only an illusion.
By default, the presentation layer is removed once the animation is complete, and your layer seems to snap back to it's pre-animation state, as you've found. There are settings you can apply that cause the animation layer to stick around in it's final state once the animation is complete, but resist the urge to do that.
Instead, what you want to do is to set your layer's properties to their end state explicitly right after the animation starts. Making that more complicated, though, is the fact that a fair number of layer properties are "implicitly animated", meaning that if you change them, the system creates an animation that makes that change for you. (In that case, the change "sticks" once the implicit animation is complete.)
You are animating strokeColor and FillColor, which are both animatable. It would be easier to take advantage of the implicit animation of those layer properties. The tricky part there is that if you want to change any of the default values (like duration) for implicit animations, you have to enclose your changes to animatable properties between a call to CATransaction.begin() and CATransaction.commit(), and use calls to CATransaction to change those values.
Here is what your code to animate your layer's strokeColor and fillColor might look like using implicit animation and a CATransaction:
CATransaction.begin()
CATransaction.setAnimationDuration(0.8)
layer.strokeColor = UIColor.red.cgColor // Your strokeColor was already red?
layer.fillColor = UIColor.lightGray.cgColor
CATransaction.commit()

Drawing checkmark with stroke animation in swift

I have a button in my iOS app and I want to apply a checkmark animation when the user taps it, but I don't have any idea how would I achieve this. If anybody has an idea please help me out.
Below is a sample gif image of the animation I want to create.
To create that animation you can either animate a mask revealing the check mark from left to right, as Matt says, or you can use a CAShapeLayer to draw the check-mark, and then animate the strokeEnd property of the shape layer.
(The sliding mask approach would always reveal the view's contents from left to right¹, whether the view contained a checkmark or a picture of a kitten. The shape layer stroke approach is specific to drawing shapes using UIBezierPath (or the Core Foundation equivalent, CGPath), but you can use it to draw complex shapes from beginning to end, even shapes that loop back on themselves. It's not limited to always revealing the view content from left to right.)
As it happens, the image you used in your question is the product of a demo app I created last weekend that uses the CAShapeLayer approach.
You can find the complete demo app here:
https://github.com/DuncanMC/AnimateCheckMark.git
In order to do animate the stroke, you'll need to create your check mark as a shape layer. Sample code to create a check mark looks like this:
private func checkmarkPath() -> CGPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: bounds.midY))
path.addLine(to: CGPoint(x: 25, y: bounds.midY + 20))
path.addLine(to: CGPoint(x: 45, y: bounds.midY - 20))
return path.cgPath
}
(That code isn't very general-purpose. It creates a fixed sized check mark. You might need to modify it to scale the check-mark to fit the view it belongs to.)
Once you have created a shape layer and installed your checkmark in it you need to create an animation that animates the shape layer's strokeEnd property. That code might look like this:
This function animates showing or hiding the checkmark view's layer by animating the layer's strokeEnd property.
- Parameter show: If true, show the checkmark. If false, hide it.
- Parameter animationDuration: the duration of the animation, in seconds.
- Parameter animationCurve: Indicates the animation timing curve to use, .linear or .easeInEaseOut
*/
public func animateCheckmarkStrokeEnd(_ show: Bool,
animationDuration: Double = 0.3,
animationCurve: AnimationCurve = .linear) {
guard let layer = layer as? CAShapeLayer else { return }
let newStrokeEnd: CGFloat = show ? 1.0 : 0.0
let oldStrokeEnd: CGFloat = show ? 0.0 : 1.0
let keyPath = "strokeEnd"
let animation = CABasicAnimation(keyPath: keyPath)
animation.fromValue = oldStrokeEnd
animation.toValue = newStrokeEnd
animation.duration = animationDuration
let timingFunction: CAMediaTimingFunction
if animationCurve == .linear {
timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
} else {
timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
}
animation.timingFunction = timingFunction
layer.add(animation, forKey: nil)
DispatchQueue.main.async {
layer.strokeEnd = newStrokeEnd
}
self.checked = show
}
The entire demo app lets you create the checkmark animation as either a stroke animation or a cross-fade, and allows you to use either linear timing or ease-in/ease-out timing. The window for the demo app looks like this:
¹: You could create other mask animations that reveal your view's contents from right to left, top to bottom, from the center out, etc. For the checkmark animation in your question, though, the mask animation you would need would be a left-to-right "wipe" animation.

How to set different animation.duration times for point 1 to 2 and point 2 to 3 of an imageView following a BezierPath in Swift?

I have a UIView subview with a BezierPath and an ImageView that follows the path from a to b. The path has 8 points and I need point 1 to point 2 to have a different duration that point 2 to point 3 etc
I've been able to set up the view and path and animate the image along the path but with the same duration time for the full animation. Here is my code for the animation, any help would be greatly appreciated.
let pathToFollow = path
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation.path = pathToFollow?.cgPath
animation.duration = 10.0
animation.calculationMode = kCAAnimationPaced
animation.delegate = self
self.icons[Icon].layer.add(animation, forKey: nil)
You have a keyframe animation, which is a good start. But you forgot to add some actual frames! To do so, supply some keyTimes. (And remove the kCAAnimationPaced; it means that the whole animation has a single speed, which is the opposite of what you said you wanted.)
Note that with a path animation of this sort, the frame division points are the bezier points used to define the path, so everything will depend on how the path was constructed in the first place.
Here's an actual example:
Observe how we speed up at each corner as we proceed round the square. The corners are where the control points are. Here's the code I used:
let path = CGPath(rect: CGRect(x: 120, y: 120, width: 100, height: 100),
transform: nil)
let anim = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
anim.duration = 10
anim.path = path
anim.calculationMode = .linear
anim.keyTimes = [0.0, 0.7, 0.9, 0.98, 1.0]
self.v.layer.add(anim, forKey: nil)

CABasicAnimation duration & keeping layer position

I'm trying to make very basic animation for UIButton. The goal is rotate layer 180 degrees. Here is my animation code which is called from beginTrackingWithTouch:
private func rotateButton() {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.timingFunction = CAMediaTimingFunction(name: "easeIn")
rotationAnimation.toValue = M_PI
rotationAnimation.duration = CollapseButton.kCollapseButtonAnimationDuration
rotationAnimation.repeatCount = 1
rotationAnimation.cumulative = true
layer.addAnimation(rotationAnimation, forKey: "rotationAnimation")
}
Now I'd like to add collapsing view animation when tapping this button. In my VC:
__weak __typeof(self) weakSelf = self;
[UIView animateWithDuration:CollapseButton.kCollapseButtonAnimationDuration animations:^{
CGRect currentFrame = weakSelf.frame;
currentFrame.size.height = 20;
weakSelf.frame = currentFrame;
}];
I have 2 questions:
After button finishes its animation it resets layer position. So, if arrow were showing top, it animated to showing down and finally resets to top. How can I preserve layer orientation?
As you can see animation duration and timing functions are the same. For the reason I cannot understand UIView animates much slower. Any ideas?
Core animation is strange. The animation creates a "presentation layer" that generates the appearance of the change you are animating, but does not actually change the property in question.
In order to get your animation to finish with the object at the end state, you should set both a fromValue (at the starting setting) and a toValue, and then set the property to it's ending value after submitting the animation:
private func rotateButton() {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.timingFunction = CAMediaTimingFunction(name: "easeIn")
rotationAnimation.fromValue = 0.0 //<<--- NEW CODE
rotationAnimation.toValue = M_PI
rotationAnimation.duration = CollapseButton.kCollapseButtonAnimationDuration
rotationAnimation.repeatCount = 1
rotationAnimation.cumulative = true
layer.addAnimation(rotationAnimation, forKey: "rotationAnimation")
layer.transform = CATransformMakeRotation(M_PI) //<<--- NEW CODE
}
You can also set the animation's removeWhenFinished property to true, but that has other complications.
BTW, you should not try to manipulate the frame of a view that has a non-identity transform. Instead, set the scale on the view's transform.
I'm not sure why the 2 animations are taking different amounts of time. I do notice that you are setting the CAAnimation's timing function to easeIn, but leaving the UIView's timing function as the default (ease-in, ease-out.) That will create animations that don't look the same. You should probably set your view animation to use easeIn timing as well. (To do that you'll need to use the longer form animateWithDuration:delay:options:animations:completion:)

Why is my CAShapeLayer not animating? (iOS Swift)

In one of my ViewControllers, I have addStatus(), which is pasted below, in my viewDidLoad. I expect the lineWidth of this circle to animate, but it doesn't seem to be doing so. In my search for why this might be, I found this part of Apple's documentation on Animating Layer Content. The part that I thought to be important noted here:
If you want to use Core Animation classes to initiate animations, you must issue all of your Core Animation calls from inside a view-based animation block. The UIView class disables layer animations by default but reenables them inside animation blocks. So any changes you make outside of an animation block are not animated. Listing 3-5 shows an example of how to change a layer’s opacity implicitly and its position explicitly. In this example, the myNewPosition variable is calculated beforehand and captured by the block. Both animations start at the same time but the opacity animation runs with the default timing while the position animation runs with the timing specified in its animation object.
When I looked up why this might not be animating, I read this piece and assumed that it meant I should place my CAAnimation inside of a UIView animation block. This function worked fine in a blank application without the animation block when placed in the rootViewController, but does not seem to animate when in my secondary viewController in this app. Any tips would be wonderfully helpful. Thanks!
func addStatus() {
UIView.animateWithDuration( NSTimeInterval.infinity, animations: { () -> Void in
let patientZeroIndicator = CAShapeLayer()
let radius:CGFloat = 20.0
let center:CGPoint = CGPointMake(self.view.frame.width - radius - 10, radius + 10)
let startAngle = 0.0
let endAngle = 2.0 * Double(M_PI)
patientZeroIndicator.lineWidth = 10.0
patientZeroIndicator.fillColor = UIColor(netHex: cs_red).CGColor
patientZeroIndicator.strokeColor = UIColor.whiteColor().CGColor
patientZeroIndicator.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true).CGPath
self.view.layer.addSublayer(patientZeroIndicator)
// Create a blank animation using the keyPath "cornerRadius", the property we want to animate
let pZeroAnimation = CABasicAnimation(keyPath: "lineWidth")
// Define the parameters for the tween
pZeroAnimation.fromValue = 10.0
pZeroAnimation.toValue = 5.0
pZeroAnimation.autoreverses = true
pZeroAnimation.duration = 3.0
pZeroAnimation.repeatDuration = CFTimeInterval.infinity
pZeroAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.75, 1)
// Finally, add the animation to the layer
patientZeroIndicator.addAnimation(pZeroAnimation, forKey: "lineWidth")
})
}
in my viewDidLoad.
That is your problem. The viewDidLoad method runs before the view has been added to a window. You can't add animations to a view unless it's in a window. Call addStatus in viewDidAppear: instead.
Also, don't create new layers in your animation block. Create the layer in viewDidLoad.

Resources