CATextLayer - how to disable implicit animations? - ios

This drives me mad. Whenever I change value of CATextLayer.foregroundColor property the change is animated no matter what I do. For example I have CATextLayer called layer which is sublayer of CALayer superlayer:
layer.removeAllAnimations()
superlayer.removeAllAnimations()
superlayer.actions = ["sublayers" : NSNull()]
CATransaction.begin()
CATransaction.setAnimationDuration(0)
CATransaction.disableActions()
CATransaction.setValue(kCFBooleanTrue, forKey:kCATransactionDisableActions)
layer.actions = ["foregroundColor" : NSNull()]
layer.actions = ["content" : NSNull()]
layer.foregroundColor = layoutTheme.textColor.CGColor
CATransaction.commit()
And it will still animates! Please help, how can I disable the implicit animations?

The problem is that CATransaction.disableActions() does not do what you think it does. You need to say CATransaction.setDisableActions(true).
And then you can get rid of all the other stuff you're saying, as it is pointless. This code alone is sufficient to change the color without animation:
CATransaction.setDisableActions(true)
layer.foregroundColor = UIColor.redColor().CGColor // or whatever
(You can wrap it in a begin()/commit() block if you have some other reason to do so, but there is no need just in order to switch off implicit layer property animation.)

Related

ios swift 4 Google Maps - How do I remove the fade in/fade out animation on a GMS Marker while dragging?

I'm trying to achieve an affect on ios that was pretty easy to do in android. In android, Google Map markers have a visibility attribute (boolean) so it was easy, but the closest thing I've found in the ios SDK is the opacity field.
Whenever I set my opacity to zero, there's a fade out affect that isn't what I want.
Is there anyway to simply remove the fade animation on a Marker?
Thank you for any insights
-T
Try use the animation keypath layer, it's work for me
CATransaction.begin()
let markerLayer = marker.layer
let fadeOutAnimation = CABasicAnimation()
fadeOutAnimation.keyPath = "opacity"
fadeOutAnimation.fromValue = 1
fadeOutAnimation.toValue = 0
fadeOutAnimation.duration = 0.35
CATransaction.setCompletionBlock {
marker.map = nil
}
markerLayer.add(fadeOutAnimation, forKey: "fade")
CATransaction.commit()
In order to animate (or not) a GMSMarker you need to control its tracksViewChanges property:
Controls whether the icon for this marker should be redrawn every
frame.
Note that when this changes from NO to YES, the icon is
guaranteed to be redrawn next frame.
Defaults to YES.
Once you create an instance of GMSMarker first, set its tracksViewChanges to false - should solve your issue since the iconView property won't get redrawn again
Also, you can animate its iconView property by:
Set its tracksViewChanges to true
Use the iconView reference to animate whatever you need
When you're done, set its tracksViewChanges to false

CAShapeLayer, animating path with Transactions

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.

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

Animating UILabel element with CoreAnimation in Swift

I'm learning how to use CoreAnimation with various UI elements. Ultimately I would like to perform complex, queued animations on all ui elements on screen as I transition to the next view.
Now however I am attempting something really simple. I want to transform (scale up) a UILabel on button tap.
I have the following function:
func animateUIElementsOut(element : AnyObject) {
var animation : CABasicAnimation = CABasicAnimation(keyPath: "transform");
var transform : CATransform3D = CATransform3DMakeScale(2, 2, 1);
animation.setValue(NSValue(CATransform3D: transform), forKey: "scaleText");
animation.duration = 2.0;
element.layer?.addAnimation(animation, forKey: "scaleText");
}
I am calling the function on a button tap like this:
animateUIElementsOut(greetingsLabel);
The compiler isn't giving off any warnings, however the animation is not working. What am I doing wrong here?
As per #David Rönnqvist suggestions the working code looks like this (fromValue / toValue added):
func animateUIElementsOut(element : AnyObject) {
var animation : CABasicAnimation = CABasicAnimation(keyPath: "transform");
animation.delegate = self;
var transform : CATransform3D = CATransform3DMakeScale(2, 2, 1);
animation.fromValue = NSValue(CATransform3D: CATransform3DIdentity);
animation.toValue = NSValue(CATransform3D: transform);
animation.duration = 2.0;
element.layer?.addAnimation(animation, forKey: nil);
}
The animation doesn't have neither a toValue or a fromValue.
You are setting the transform for the "scaleText" key on the animation but the animation doesn't have a corresponding property.
To make the animation itself work you should set the transform as the toValue of the animation. However, there are still things missing to make the whole thing work. The model value of the layer is never changed so when the animation finished and is removed, you will see the old (model) values again.
If you are only interested in making a scaling animation, I would recommend that you use the higher level UIView animation APIs instead:
UIView.animateWithDuration(2.0) {
element.transform = CGAffineTransformMakeScale(2, 2)
}
If on the other hand you are interested in working with Core Animation, there are a couple more things to do to make it all work.
configure the animation to go from the current model value (using the fromValue property)
configure the animation to go to the new value (as explained above)
update the model value to the new value before adding the animation.
As a side trivia, the reason that you could set a value for "scaleText" without there being any such property is that layers and animations work a bit differently than most classes with key-value coding. You can set and get any value on both a layer and a view using key-value coding. This means that this is valid code:
let layer = CALayer()
layer.setValue(1, forKey: "foo")
var foo = layer.valueForKey("foo") // is equal to 1
let anim = CABasicAnimation(keyPath: "bounds")
anim.setValue(2, forKey: "bar")
var bar = anim.valueForKey("bar") // is equal to 2
However, if you try and do the same on almost any other object, you will get a runtime exception saying that the class isn't key-value coding compliant for that key. For example, this code:
let view = UIView(frame: CGRectZero)
view.setValue(3, forKey: "baz")
Produces this runtime exception:
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<UIView 0x10dc08b30> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key baz.'

Get current CAAnimation transform value

I want to access get the value of the transform scale at a point in time. Here is the animation creation :
CABasicAnimation *grow = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
grow.toValue = #1.5f;
grow.duration = 1;
grow.autoreverses = YES;
grow.repeatCount = HUGE_VALF;
[view.layer addAnimation:grow forKey:#"growAnimation"];
I'd like to get, for example when a user presses a button, the current size of the view.
Logging the frame or the bounds always returns constant values. Any help would be much appreciated !
In Core Animation if an animation is "in flight" on a layer, the layer has a second layer property known as presentationLayer. That layer contains the values of the in-flight animation.
Edited
(With a nod to Mohammed, who took the time to provide an alternate answer for Swift)
Use code like this:
Objective-C
CGFloat currentScale = [[layer.presentationLayer valueForKeyPath: #"transform.scale"] floatValue];
Swift:
let currentScale = layer.presentation()?.value(forKeyPath: "transform.scale") ?? 0.0
Note that the Swift code above will return 0 if the layer does not have a presentation layer. That's a fail case that you should program for. It might be better to leave it an optional and check for a nil return.
Edit #2:
Note that as Kentzo pointed out in their comment, reading a value from an in-flight animation will give you a snapshot at an instant in time. The new value of the parameter you read (transform.scale in this example) will start to deviate from the reading you get. You should either pause the animation, or use the value you read quickly and get another value each time you need it.
The CALayer documentation describes presentationLayer quite clearly:
The layer object returned by this method provides a close approximation of the layer that is currently being displayed onscreen. While an animation is in progress, you can retrieve this object and use it to get the current values for those animations.
swift version of #Duncan C answer will be:
let currentValue = someView.layer.presentation()?.value(forKeyPath: "transform.scale")
Swift 5: The nicest answer, in a function from Hacking With Swift
func scale(from transform: CGAffineTransform) -> Double {
return sqrt(Double(transform.a * transform.a + transform.c * transform.c))
}

Resources