CAShapeLayer, animating path with Transactions - ios

I wondering why when I try to animate the path property of a CAShapeLayerwith a basic animation it works but when I try to do it with transaction it doesn't.
I've successfully animated other animatable properties using just transaction. Here is my current code:
CATransaction.begin()
CATransaction.setAnimationDuration(2.0)
path = scalePath() // a scaled version of the original path
CATransaction.commit()
the new path is obtained scaling the original path with this (very hardcoded) function inside an extension of CAShapeLayer:
func scalePath()->CGPath{
var scaleTransform = CGAffineTransform.identity.translatedBy(x: -150, y: -150)
scaleTransform = scaleTransform.scaledBy(x: 10, y: 10)
let newPath = path?.copy(using: &scaleTransform)
return newPath!
}
Can you identify any issue?

The answer is simple but a bit unsatisfactory: while the path property is animatable, it doesn't support implicit animations. This is called out in the Discussion section of the documentation for the path property:
Unlike most animatable properties, path (as with all CGPathRef animatable properties) does not support implicit animation.
Explicit vs. Implicit animations
An "explicit" animation is an animation object (e.g. CABasicAnimation) that is explicitly added to the layer by calling -addAnimation:forKey: on the layer.
An "implicit" animation is an animation that happens implicitly as a result of changing an animatable property.
The animation is considered implicit even if the property is changed within a transaction.

Related

draw#rect core graphics, on top of a layer?

Say you have this in a UIView,
override func draw(_ rect: CGRect) {
let c = UIGraphicsGetCurrentContext()
c?.setLineWidth(10.0)
c?.move(to: CGPoint(x: 10.0, y: 10.0))
c?.addLine(to: CGPoint(x: 40.0, y: 40.0))
... lots of complicated drawing here, sundry paths, colors, widths etc ...
c?.strokePath()
}
Of course, it will draw the hell out of your drawing for you.
But say in the same UIView you do this ...
func setup() { // in inits, layout
if nil etc {
thingLayer = CAShapeLayer()
self.layer.insertSublayer(thingLayer, at: 0)
thingLayer.fillColor = UIColor.yellow.cgColor }
thingLayer.frame = bounds
let path = ... some fancy path
thingLayer.path = path.cgPath
}
Indeed, the new yellow layer is drawn over the drawing in draw#rect.
How do you draw - using core graphics commands - either on to thingLayer, or perhaps on to another layer on top of all??
Core graphics commands:
let c = UIGraphicsGetCurrentContext()
c?.setLineWidth(10.0)
c?.move(to: CGPoint(x: 10.0, y: 10.0))
c?.addLine(to: CGPoint(x: 40.0, y: 40.0))
c?.strokePath()
seem to draw to a place directly above or on the main .layer
(Well that appears to be the case, as far as I can see.)
How do you draw - using core graphics commands - either on to thingLayer, or perhaps on to another layer on top of all??
Surely in draw#rect you can specify which cgLayer to draw to?
In draw#rect, can you make another cgLayer and draw to context, that cgLayer?
Bottom line, the draw(_:) is for rendering the root view of a UIView subclass. If you add sublayers, they will be rendered on top of whatever is done in the root view’s draw(_:).
If you have a few paths currently performed in the draw(_:) that you want to render on top of the sublayer that you’ve added, you have a few options:
Move the paths to be stroked in their own CAShapeLayer instances and add them above the other sublayer that you’ve already added.
Consider the main UIView subclass as a container/content view, and add your own private UIView subviews (potentially with the “complicated” draw(_:) methods). You don’t have to expose these private subviews if you don’t want to.
Move the complicated drawing to the draw(in:) of a CALayer subclass and, again, add this sublayer above the existing sublayer that you’ve already created.
Given that the thingLayer is, effectively, only setting the background color, you could just set the background color of the view and not use a layer for this background color at all.
The CALayer hierarchy is a tree, where a root layer is first (e.g. the layer that comprises the background of a window), followed by its sublayers (an array of CALayer objects, stored and drawn in in back-to-front order), followed by their sublayers, and so on, recursively.
Your implementation of UIView.draw(_ rect: CGRect) is defining how the main layer of your view (self.layer) is drawn.
By adding your CAShapeLayer as a sublayer of self.layer, you're making it be draw after self.layer, which has the effect of it being drawn above self.layer, which contains the line you drew in UIView.draw(_ rect: CGRect).
To resolve this, you need to put your stroke in sublayer after your thingLayer.
self.layer.insertSublayer(thingLayer, at: 0)
self.layer.insertSublayer(lineLayer, above: thingLayer)
By the way, you forgot to delegate to super.draw(rect). It's not necessary for direct subclasses of UIView (since the base implementation doesn't do anything), but it is necessary for every other case, and it's generally a good habit to get into (lest you run into really obscure/frustrating drawing bugs).
If your yellow shape layer doesn't change or move around independently, you could get rid of the CAShapeLayer and just draw the shape yourself at the top of your implementation of draw(_:):
let path = // your fancy UIBezierPath
UIColor.yellow.setFill()
path.fill()
Otherwise, as other commenters have said, you'll need to move your drawing code into additional custom subclasses of UIView or CALayer, and stack them in the order you want.

How to animate drawing in Swift, but also change a UIImageView's scale?

I'd like to animate a drawing sequence. My code draws a spiral into a UIImageView.image. The sequence changes the image contents, but also changes the scale of the surrounding UIImageView. The code is parameterized for the number of revolutions of the spiral:
func drawSpiral(rotations:Double) {
let scale = scaleFactor(rotations) // do some math to figure the best scale
UIGraphicsBeginImageContextWithOptions(mainImageView.bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.scaleBy(x: scale, y: scale) // some animation prohibits changes!
// ... drawing happens here
myUIImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}
For example, I'd like to animate from drawSpiral(2.0) to drawSpiral(2.75) in 20 increments, over a duration of 1.0 seconds.
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values? How? Is there a better animation approach?
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values
Animation is merely a succession of timed intermediate values being thrown at something. It is perfectly reasonable to ask that they be thrown at your code so that you can do whatever you like with them. Here's how.
You'll need a special layer:
class MyLayer : CALayer {
#objc var spirality : CGFloat = 0
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(spirality) {
return true
}
return super.needsDisplay(forKey:key)
}
override func draw(in con: CGContext) {
print(self.spirality) // in real life, this is our signal to draw!
}
}
The layer must actually be in the interface, though it can be impossible for the user to see:
let lay = MyLayer()
lay.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
self.view.layer.addSublayer(lay)
Subsequently, we can initialize the spirality of the layer:
lay.spirality = 2.0
lay.setNeedsDisplay() // prints: 2.0
Now when we want to "animate" the spirality, this is what we do:
let ba = CABasicAnimation(keyPath:#keyPath(MyLayer.spirality))
ba.fromValue = lay.spirality
ba.toValue = 2.75
ba.duration = 1
lay.add(ba, forKey:nil)
CATransaction.setDisableActions(true)
lay.spirality = 2.75
The console shows the arrival of a succession of intermediate values over the course of 1 second!
2.03143266495317
2.04482554644346
2.05783333256841
2.0708108600229
2.08361491002142
2.0966724678874
2.10976020619273
2.12260236591101
2.13551922515035
2.14842618256807
2.16123360767961
2.17421661689878
2.18713565543294
2.200748950243
2.21360073238611
2.2268518730998
2.23987507075071
2.25273013859987
2.26560932397842
2.27846492826939
2.29135236144066
2.30436328798532
2.31764804571867
2.33049770444632
2.34330793470144
2.35606706887484
2.36881992220879
2.38163591921329
2.39440815150738
2.40716737508774
2.42003352940083
2.43287514150143
2.44590276479721
2.45875595510006
2.47169743478298
2.48451870679855
2.49806520342827
2.51120449602604
2.52407149970531
2.53691896796227
2.54965999722481
2.56257836520672
2.57552136480808
2.58910304307938
2.60209316015244
2.6151298135519
2.62802086770535
2.64094598591328
2.6540260463953
2.6669240295887
2.6798157542944
2.69264766573906
2.70616912841797
2.71896715462208
2.73285858333111
2.74564798176289
2.75
2.75
2.75
Those are exactly the numbers that would be thrown at an animatable property, such as when you change a view's frame origin x from 2 to 2.75 in a 1-second duration animation. But now the numbers are coming to you as numbers, and so you can now do anything you like with that series of numbers. If you want to call your method with each new value as it arrives, go right ahead.
Personally, in more complicated animations I would use lottie the animation itself is built with Adobe After Effect and exported as a JSON file which you will manage using the lottie library this approach will save you time and effort when you port your app to another platform like Android as they also have an Android Lottie which means the complicated process of creating the animation is only done once.
Lottie Files has some examples animations as well for you to look.
#Matt provided the answer and gets the checkmark. I'll recap some points for emphasis:
UIView animation is great for commonly animated properties, but if
you need to vary a property not on UIView's animatable list, you can't use it. You must
create a new CALayer and add a CABasicAnimation(keyPath:) to it.
I tried but was unable to get my CABasicAnimations to fire by adding them to the default UIView.layer. I needed to create a custom CALayer
sublayer to the UIView.layer - something like
myView.layer.addSublayer(myLayer)
Leave the custom sublayer installed and re-add the CABasicAnimation to that sublayer when (and only when) you want to animate drawing.
In the custom CALayer object, be sure to override class func needsDisplay(forKey key: String) -> Bool with your key property (as #Matt's example shows), and also override func draw(in cxt: CGContext) to do your drawing. Be sure to decorate your key property with #objc. And reference the key property within the drawing code.
A "gotcha" to avoid: in the UIView object, be sure to null out the usual draw method (override func draw(_ rect: CGRect) { }) to avoid conflict between animated and non-animated drawing on the separate layers. For coordinating animated and non-animated content in the same UIView, it's good (necessary?) to do all your drawing from your custom layer.
When doing that, use myLayer.setNeedsDisplay() to update the non-animated content within the custom layer; use myLayer.add(myBasicAnimation, forKey:nil) to trigger animated drawing within the custom layer.
As I said above, #Matt answered - but these items seemed worth emphasizing.

Custom animation of two properties in CALayer by a new custom property

I can animate a CALayer object by changing a property using CABasicAnimation as with this code:
let animationOfPropertyOpacity = CABasicAnimation(keyPath: "opacity")
animationOfPropertyOpacity.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animationOfPropertyOpacity.fromValue = 0
animationOfPropertyOpacity.toValue = 1
myLayer.addAnimation(animationOfPropertyOpacity, forKey: "fade")
In this case the animation will create a linear interpolation between 0 to 1. I can do the same with other properties like scale, position or rotation.
What If I want to move from point A (0,3) to point B (3,0) but making a circle respect to point C (0,0)?
I want to keep using fromValue to toValue, but animating a custom property that I've made called rotation. Rotation will calculate each interpolation value based on point C as center and making a radius from that center.
Where do I need to put the function that calculates each step?
I've tried subclassing CALayer and adding a custom property called rotation. Then adding code to needsDisplayForKey to redraw the layer:
#NSManaged var rotation: CGFloat
override class func needsDisplayForKey(key: String) -> Bool {
if (key == "rotation") {
return true
}
return super.needsDisplayForKey(key)
}
This calls drawInContext and makes the whole CALayer black. I have a print in drawInContext and it doesn't get called when changing other properties like opacity, it seems it's not necessary to redraw the layer.
There should be a method that is being called each frame of the animation in CALayer where I could implement a function before applying the changes.
I'm also unable to find the right place to implement the functionality of the custom property that I've created to actually reflect the change even without animation. Let's say a property customOpacity that just inverts the effects of opacity and updating the layer accordingly. I only could make that through a function updateRotation(rotation: CGFloat).

Ios Swift Animate a view in non linear path

I am trying to animate a UIView through non linear path(i'm not trying to draw the path itself) like this :
The initial position of the view is determinated using a trailing and bottom constraint (viewBottomConstraint.constant == 100 & viewTrailingConstraint.constant == 300)
I am using UIView.animatedWithDuration like this :
viewTrailingConstraint.constant = 20
viewBottomConstraint.constant = 450
UIView.animateWithDuration(1.5,animation:{
self.view.layoutIfNeeded()
},completition:nil)
But it animate the view in a linear path.
You can use keyFrame animation with path
let keyFrameAnimation = CAKeyframeAnimation(keyPath:"position")
let mutablePath = CGPathCreateMutable()
CGPathMoveToPoint(mutablePath, nil,50,200)
CGPathAddQuadCurveToPoint(mutablePath, nil,150,100, 250, 200)
keyFrameAnimation.path = mutablePath
keyFrameAnimation.duration = 2.0
keyFrameAnimation.fillMode = kCAFillModeForwards
keyFrameAnimation.removedOnCompletion = false
self.label.layer.addAnimation(keyFrameAnimation, forKey: "animation")
Gif
About this function
void CGContextAddQuadCurveToPoint (
CGContextRef _Nullable c,
CGFloat cpx,
CGFloat cpy,
CGFloat x,
CGFloat y
);
(cpx,cpy) is control point,and (x,y) is end point
Leo's answer of using Core Animation and CAKeyframeAnimation is good, but it operates on the view's "presentation layer" and only creates the appearance of moving the view to a new location. You'll need to add extra code to actually move the view to it's final location after the animation completes. Plus Core Animation is complex and confusing.
I'd recommend using the UIView method
animateKeyframesWithDuration:delay:options:animations:completion:. You'd probably want to use the option value UIViewKeyframeAnimationOptionCalculationModeCubic, which causes the object to move along a curved path that passes through all of your points.
You call that on your view, and then in the animation block, you make multiple calls to addKeyframeWithRelativeStartTime:relativeDuration:animations: that move your view to points along your curve.
I have a sample project on github that shows this and other techniques. It's called KeyframeViewAnimations (link)
Edit:
(Note that UIView animations like animateKeyframes(withDuration:delay:options:animations:completion:) don't actually animate your views along the path you specify. They use a presentation layer just like CALayer animations do, and while the presentation layer makes the view look like it's moving along the specified path, it actually snaps from the beginning position to the end position at the beginning of the animation. UIView animations do move the view to its destination position, where CALayer animations move the presentation layer while not moving the layer/view at all.)
Another subtle difference between Leo's path-based UIView animation and my answer using UIView animateKeyframes(withDuration:delay:options:animations:completion:)is that CGPath curves are cubic or quadratic Bezier curves, and my answer animates using a different kind of curve called a Katmull-Rom spline. Bezier paths start and end at their beginning and ending points, and are attracted to, but don't pass through their middle control points. Catmull-Rom splines generate a curve that passes through every one of their control points.

UIDynamicItem update transform manually

I know that external change to center, bounds and transform will be ignored after UIDynamicItems init.
But I need to manually change the transform of UIView that in UIDynamicAnimator system.
Every time I change the transform, it will be covered at once.
So any idea? Thanks.
Any time you change one of the animated properties, you need to call [dynamicAnimator updateItemUsingCurrentState:item] to let the dynamic animator know you did it. It'll update it's internal representation to match the current state.
EDIT: I see from your code below that you're trying to modify the scale. UIDynamicAnimator only supports rotation and position, not scale (or any other type of affine transform). It unfortunately takes over transform in order to implement just rotation. I consider this a bug in UIDynamicAnimator (but then I find much of the implementation of UIKit Dynamics to classify as "bugs").
What you can do is modify your bounds (before calling updateItem...) and redraw yourself. If you need the performance of an affine transform, you have a few options:
Move your actual drawing logic into a CALayer or subview and modify its scale (updating your bounds to match if you need collision behaviors to still work).
Instead of attaching your view to the behavior, attach a proxy object (just implement <UIDyanamicItem> on an NSObject) that passes the transform changes to you. You can then combine the requested transform with your own transform.
You can also use the .action property of the UIDynamicBehavior to set your desired transform at every tick of the animation.
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
attachment.damping = 0.8f;
attachment.frequency = 0.8f;
attachment.action = ^{
CGAffineTransform currentTransform = item.transform;
item.transform = CGAffineTransformScale(currentTransform, 1.2, 1.2)
};
You would need to add logic within the action block to determine when the scale should be changed, and by how much, otherwise your view will always be at 120%.
Another way to fix this (I think we should call it bug) is to override the transform property of the UIView used. Something like this:
override var transform: CGAffineTransform {
set (value) {
}
get {
return super.transform
}
}
var ownTransform: CGAffineTransform. {
didSet {
super.transform = ownTransform
}
}
It's a kind of a hack, but it works fine, if you don't use rotation in UIKitDynamics.

Resources