CATransaction completion being called immediately - ios

I'm trying to execute a completion-block after my CAAnimation has finished. However, it seems that animation block is called before my animation completes. The animation still happens correctly though.
[CATransaction begin];
[self.view.layer addAnimation:self.dropAndBounceAnimation forKey:#"appearance"];
[CATransaction setCompletionBlock:completionBlock];
[CATransaction commit];
The dropAndBounceAnimation is a CAKeyFrameAnimation on position.y, with a fixed duration.

I'm not sure if this really is the correct fix, but by setting the completion-block before adding the animation for the layer, the completion-block is consistently called at the correct time.
[CATransaction begin];
[CATransaction setCompletionBlock:completionBlock];
[self.view.layer addAnimation:self.dropAndBounceAnimation forKey:#"appearance"];
[CATransaction commit];

You need to set the completion block before adding the animation.
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat: 1.0f] forKey:kCATransactionAnimationDuration];
[CATransaction setCompletionBlock:^{
// ... whatever you want to do when the animation is complete
}];
[self.googleMapsView animateToCameraPosition:[GMSCameraPosition
cameraWithLatitude:LATITUDE
longitude:LONGITUDE
zoom:ZOOM]];
[CATransaction commit];
This must trigger the completion block after the completion of that animation on the view.

Here is Swift 3.0.1, Xcode 8 version:
CATransaction.begin()
CATransaction.setCompletionBlock({
print("Transaction completed")
})
print("Transaction started")
view.layer.add(dropAndBounceAnimation, forKey: "appearance")
CATransaction.commit()

Try to start the animation asynchronously:
DispatchQueue.main.async {
self.startAnimation()
}
because it can interfere with view drawing if you make some view setup before calling the animation.

Related

Xcode: how to do linear animation in a CATransaction

My transaction starts slow, gets fast, then goes slow.
I need linear speed throughout.
I've found UIViewAnimationOptionCurveLinear but can't find an example of [CATransaction begin]
Here is my code:
[ CATransaction begin ];
if( graph_animation_enable )
[CATransaction setAnimationDuration: graph_animation_seconds ];
else
[CATransaction setAnimationDuration: 0 ];
//[CATransaction setValue : ( id ) kCFBooleanTrue forKey : kCATransactionDisableActions];
graph_CALayer.frame = CGRectMake( left_x, top_y, width, height );
graph_CALayer.backgroundColor = bar_background_color.CGColor;
CAMediaTimingFunction *linearTiming =
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
[CATransaction setAnimationTimingFunction: linearTiming];
[CATransaction commit];
I TRIED THE ANSWER, BUT STILL NON-LINEAR.
Animation starts, but then slows down.
Use CATransaction setAnimationTimingFunction and the timing value of kCAMediaTimingFunctionLinear
The code would look like this:
CAMediaTimingFunction *linearTiming =
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
[CATransaction setAnimationTimingFunction: linearTiming]
With Swift 5, CATransaction has a method called setAnimationTimingFunction(_:). setAnimationTimingFunction(_:) has the following declaration:
class func setAnimationTimingFunction(_ function: CAMediaTimingFunction?)
Sets the timing function used for all animations within this transaction group. [...] This is a convenience method that sets the CAMediaTimingFunction for the value(forKey:) value of the kCATransactionAnimationTimingFunction key.
Therefore, you can use one of the following code snippets in order to set a linear animation for your CATransaction:
CATransaction.begin()
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear))
/* ... */
CATransaction.commit()
CATransaction.begin()
CATransaction.setValue(CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear), forKey: kCATransactionAnimationTimingFunction)
/* ... */
CATransaction.commit()

Controlling Animation Duration in Google Maps for iOS

The documentation for Google Maps for iOS states that:
Call one of several methods that allow you to animate the camera moving to a new location. You can control the duration of the animation with CoreAnimation.
For the life of me, I can't figure out how to control the animation duration. I have tried using UIView animations, like:
[UIView animateWithDuration: 5 animations:^{
GMSCameraPosition *camera = [self newCamera];
self.mapView.camera = camera;
} completion:^(BOOL finished) {
}];
And I have looked at CALayer animations in CoreAnimation. However, I don't know how you would apply a layer animation to the map view.
Can someone point me in the right direction please?
I found the answer ... you can control the animation duration by wrapping one of the animate* methods in a CATransaction, like this:
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat: 1.0f] forKey:kCATransactionAnimationDuration];
// change the camera, set the zoom, whatever. Just make sure to call the animate* method.
[self.mapView animateToCameraPosition: [self newCamera]];
[CATransaction commit];
for Swift 3.0:
CATransaction.begin()
CATransaction.setValue(1.5, forKey: kCATransactionAnimationDuration)
// your camera code goes here, example:
// mapView.animate(with: update)
CATransaction.commit()
The bigger the value (1.5 in this case), the slower the animation.
Swift 2.0
CATransaction.begin()
CATransaction.setValue(NSNumber(float: 1.0), forKey: kCATransactionAnimationDuration)
// change the camera, set the zoom, whatever. Just make sure to call the animate* method.
CATransaction.commit()
what a pitty that using the same methods you provided there is no way to know if the animation has ended.
Yes I know, there is a CATransaction completion block using this method but it does simply not work! :(
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat: 1.0f] forKey:kCATransactionAnimationDuration];
[CATransaction setCompletionBlock:^{
// ... whatever you want to do when the animation is complete
}];
[self.googleMapsView animateToCameraPosition:[GMSCameraPosition
cameraWithLatitude:LATITUDE
longitude:LONGITUDE
zoom:ZOOM]];
[CATransaction commit];
And I can't use MapView:didIdle hack to know that the animation has ended because it will not be called if there is no camera position change.
Anyone knows how to detect animateon has ended event?
FOUND A THREAD ABOUT THIS (solved):
CATransaction completion being called immediately

Delay while moving CALayer with pan gesture

I use pan gesture to move an image in CALayer. The issue I experience is that the image appears to move with a little delay and does not appear 'stuck' to my finger.
Here is the actual snippet of how I move the layer(facePic is the CALayer):
CGPoint translation =[touche locationInView:self.view];
self.facePic.frame =
CGRectMake(translation.x - self.facePic.frame.size.width/2,
translation.y - self.facePic.frame.size.height/2,
self.facePic.frame.size.width,
self.facePic.frame.size.height);
I think you see the result of implicit animation of a layer. If so then there are two options to disable this animation:
use transactions
set layer actions
To use transactions wrap your code with CATransaction
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
. . .
[CATransaction commit];
To disable some layer actions you can add this to the layer init, the position animation for example:
aLayer.actions = #{#"position":[NSNull null]}; // FIXED property name

How do I chain a sequence of animations of the same property on three different objects?

I have been trying to create a sequence of animations that deal three cards from a poker deck sequentially. I just want to animate the position of the three cards -- say the first animation begins immediately on the first card's position and goes for 0.4 seconds, the second begins after 0.4 seconds with the same duration, and the last begins after 0.8 seconds. I can't figure out how to do this! The code below doesn't work. Perhaps I need to use a CAGroupAnimation, but I don't know how to make a group of sequential
animations on the same property!
CGFloat beginTime = 0.0;
for (Card *c in cards) {
CardLayer *cardLayer = [cardToLayerDictionary objectForKey:c];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
anim.fromValue = [NSValue valueWithCGPoint:stockLayer.position];
anim.toValue = [NSValue valueWithCGPoint:wasteLayer.position];
anim.duration = 0.4;
anim.beginTime = beginTime;
beginTime += 0.4;
cardLayer.position = wasteLayer.position;
[cardLayer addAnimation:anim forKey:#"position"];
…
}
Like Einstein said, time is relative. All your layers' beginTimes are relative to the timespace of their superlayer---since it isn't animating, they all end up the same.
It looks like there are two possible solutions:
Get an absolute timebase and set each layer's animation's beginTime relative to it:
int i = 0; float delay = 0.4;
for (Card *c in cards) {
// ...
float baseTime = [cardLayer convertTime:CACurrentMediaTime() fromLayer:nil];
anim.beginTime = baseTime + (delay * i++);
// ...
}
Wrap each card's animation in a group. For some reason I don't quite get, this puts those animations on a common timescale, even though these groups are separate from one another. It's worked for some, but I'm a bit disinclined to trust it---seems like spooky action at a distance.
Either way, you're probably also going to want the layers to stay where they are at the end of the animation. You could make the animation "stick" like so:
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
But that might give your trouble later---the layer's model position is still where it was to start, and if you adjust that after the animation (say, for dragging a card around) you might get weird results. So it might be appropriate to wrap your card dealing animation in a CATransaction, on which you set a completion block that sets the layers' final positions.
Speaking of CATransactions with completion blocks, you could use those to make implicit animations happen in sequence (if you're not using explicit animation for some other reason):
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
card3Layer.position = wasteLayer.position;
}];
card2Layer.position = wasteLayer.position;
[CATransaction end];
}];
card1Layer.position = wasteLayer.position;
[CATransaction end];
The following recursive method is the best I could do to chain animations. I use a CATransaction to animate the position property and set up a block that makes a recursive call for the next card.
-(void)animateDeal:(NSMutableArray*)cardLayers {
if ([cardLayers count] > 0) {
CardLayer *cardLayer = [cardLayers objectAtIndex:0];
[cardLayers removeObjectAtIndex:0];
// ...set new cardLayer.zPosition with animations disabled...
[CATransaction begin];
[CATransaction setCompletionBlock:^{[self animateDeal:cardLayers];}];
[CATransaction setAnimationDuration:0.25];
[cardLayer setFaceUp:YES];
cardLayer.position = wasteLayer.position;
[CATransaction commit];
}
}
Of course this doesn't solve the chaining problem with overlapping time intervals.

How do you move a CALayer instantly (without animation)

I'm trying to drag a CALayer in an iOS app.
As soon as I change its position property it tries to animate to the new position and flickers all over the place:
layer.position = CGPointMake(x, y)
How can I move CALayers instantly? I can't seem to get my head around the Core Animation API.
You want to wrap your call in the following:
[CATransaction begin];
[CATransaction setValue: (id) kCFBooleanTrue forKey: kCATransactionDisableActions];
layer.position = CGPointMake(x, y);
[CATransaction commit];
Swift 3 Extension :
extension CALayer {
class func performWithoutAnimation(_ actionsWithoutAnimation: () -> Void){
CATransaction.begin()
CATransaction.setValue(true, forKey: kCATransactionDisableActions)
actionsWithoutAnimation()
CATransaction.commit()
}
}
Usage :
CALayer.performWithoutAnimation(){
someLayer.position = newPosition
}
You can also use the convenience function
[CATransaction setDisableActions:YES]
as well.
Note: Be sure to read the comments by Yogev Shelly to understand any gotchas that could occur.
As others have suggested, you can use CATransaction.
The problem comes arises because CALayer has a default implicit animation duration of 0.25 seconds.
Thus, an easier (in my opinion) alternative to setDisableActions is to use setAnimationDuration with a value of 0.0.
[CATransaction begin];
[CATransaction setAnimationDuration:0.0];
layer.position = CGPointMake(x, y);
[CATransaction commit];
Combining previous answers here for Swift 4, to clearly make the animation duration explicit...
extension CALayer
{
class func perform(withDuration duration: Double, actions: () -> Void) {
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
actions()
CATransaction.commit()
}
}
Usage...
CALayer.perform(withDuration: 0.0) {
aLayer.frame = aFrame
}

Resources