Animation subview removal freezes when scrolling - ios

My code:
I am using the following code for an add to basket animation:
-(void)triggerAddItemAnimation:(NSIndexPath*)indexPath
{
//Cell that was pressed.
CellMenuItem *cell = [self.tableView cellForRowAtIndexPath:indexPath];
// grab the imageview using cell
UIImageView *imgV = (UIImageView*)[cell viewWithTag:IDENTIFIER_ANIMATION_IMAGE];
//Get the x-coordinate of the image.
int xCoord = cell.imgAddButton.frame.origin.x;
// get the exact location of image
CGRect rect = [imgV.superview convertRect:imgV.frame fromView:nil];
rect = CGRectMake(xCoord, (rect.origin.y*-1)-10, imgV.frame.size.width, imgV.frame.size.height);
//NSLog(#"rect is %f,%f,%f,%f",rect.origin.x,rect.origin.y,rect.size.width,rect.size.height);
// create new duplicate image
UIImageView *starView = [[UIImageView alloc] initWithImage:imgV.image];
[starView setFrame:rect];
starView.layer.cornerRadius=5;
starView.layer.borderColor=[[UIColor blackColor]CGColor];
starView.layer.borderWidth=1;
[self.view addSubview:starView];
// begin ---- apply position animation
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration=0.65;
pathAnimation.delegate=self;
// tab-bar right side item frame-point = end point
CGPoint endPoint = CGPointMake(self.lblBasketItemsCount.frame.origin.x, self.lblBasketItemsCount.frame.origin.y);
CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, starView.frame.origin.x, starView.frame.origin.y);
CGPathAddCurveToPoint(curvedPath, NULL, endPoint.x, starView.frame.origin.y, endPoint.x, starView.frame.origin.y, endPoint.x, endPoint.y);
pathAnimation.path = curvedPath;
CGPathRelease(curvedPath);
// end ---- apply position animation
float animationDuration = 0.65f;
// apply transform animation
CABasicAnimation *basic=[CABasicAnimation animationWithKeyPath:#"transform"];
[basic setToValue:[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.25, 0.25, 0.25)]];
[basic setAutoreverses:NO];
[basic setDuration:animationDuration];
[starView.layer addAnimation:pathAnimation forKey:#"curveAnimation"];
[starView.layer addAnimation:basic forKey:#"transform"];
//Remove the animation 0.05 before the animationDuration to remove clipping issue.
[starView performSelector:#selector(removeFromSuperview) withObject:nil afterDelay:animationDuration - 0.05f];
}
The Problem:
The animation works beautifully but when I scroll the image used for the animation (taken from the cell) does not remove from the view until the scrolling has stopped.
The subview being removed is removed with the last line of code within the method:
[starView performSelector:#selector(removeFromSuperview) withObject:nil afterDelay:animationDuration - 0.05f];
What I've tried:
Ive tried using GCD:
dispatch_async(dispatch_get_main_queue(), ^{
//call method here....
});
If I recall I believe Ive also tried a run loop with a timer, as well as perform selector on main thread.
Ive also tried putting the method in a block(not the actual code I used, merely an example):
[UIView animateWithDuration:0.5
delay:1.0
options: UIViewAnimationCurveEaseOut
animations:^{
}
completion:^(BOOL finished){
//Remove subview here
}];
The Question:
I can deduce that the scrolling is tying up the main thread, hence why I've tried to call the method asynchronously.
Can someone let me know what I'm doing wrong ?

To solve this issue you can use the complete portion of a block as can be seen above. I was simply using the block incorrectly at the time.
However the way I solved this, because I couldn't get a path animation to work in a block was to set the delegate of the animation to self.
And use the following:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
[theView removeFromSuperview];
}

Related

How can I trigger my animation after a specific segue?

I have an animation that is currently working. I followed this answer Xcode How to move an image up and down animation
I was able to get the animation triggered with a button. What I really want is to trigger the animation after a specific segue. Is it possible to trigger this animation when I segue to a specific view in my storyboard?
The animation is in a method inside my ViewController implementation.
-(void)animatePicker
{
NSLog(#"Animate");
CGPoint startPoint = (CGPoint){100.f, 100.f};
CGPoint middlePoint = (CGPoint){400.f, 400.f};
CGPoint endPoint = (CGPoint){600.f, 100.f};
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath, NULL, startPoint.x, startPoint.y);
CGPathAddLineToPoint(thePath, NULL, middlePoint.x, middlePoint.y);
CGPathAddLineToPoint(thePath, NULL, endPoint.x, endPoint.y);
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.duration = 3.f;
animation.path = thePath;
[pickerCircle.layer addAnimation:animation forKey:#"position"];
pickerCircle.layer.position = endPoint;
}
Implement a -(void)viewDidAppear; method in your UIViewController and call your animation function there.
This function is called when UIViewController view has been created and has appeared as well.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(#"viewDidAppear");
[masterViewController animate];
}

AVFoundation focus animation using Core Animation

So,I want to show a square box, a I typical focus animation when a user taps on the screen. Here is what I have tried:
-(void)showFocusAnimation:(CGPoint)location{
UIView *square = [[UIView alloc]initWithFrame:CGRectMake(location.x, location.y, 40, 40)];
square.alpha=1.0;
square.layer.borderColor = (__bridge CGColorRef)[UIColor colorWithRed:12.0/255.0 green:185.0/255.0 blue:249.0/255.0 alpha:1];
square.layer.borderWidth = 2.0;
[overlayView addSubview:square];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:.1];
square.frame = CGRectMake(location.x, location.y, 90, 90);
[UIView commitAnimations];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:.1];
square.frame = CGRectMake(location.x, location.y, 40, 40);
[UIView commitAnimations];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:.2];
square.frame = CGRectMake(location.x, location.y, 90, 90);
square.alpha = 0;
[UIView commitAnimations];
}
I have a couple of problems that I cant seem to solve :
I can't get my border to show up.
Currently I am drawing a square starting from the point where the user tapped the screen. The point at which the user taps it, should actually be the center of the square.
I can't seem to get the animation correct. What I am trying to do is, decrease the square size, increase it and then decrease it again and then the alpha = 0.
I thought if I have 3 different separate animations maybe it will work, is not working.
Your problem is that triggering animations is asynchronous so they all start at the same time and first two frame animations are replaced by the third.
One thing that you could do instead is to use Core Animation (your question actually uses UIView animation and the not even the new block stuff) to create an animation group for the size and opacity animations. It would look something like this (note I didn't run this so it may contain typos and such)
CAKeyframeAnimation *resize = [CAKeyframeAnimation animationWithKeyPath:#"bounds.size"];
resize.values = #[[NSValue valueWithCGSize:CGSizeMake(40, 40)],
[NSValue valueWithCGSize:CGSizeMake(90, 90)],
[NSValue valueWithCGSize:CGSizeMake(40, 40)],
[NSValue valueWithCGSize:CGSizeMake(90, 90)]];
resize.keyTimes = #[#0, #.25, #.5, #1];
CABasicAnimation *fadeOut = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeOut.toValue = #0;
fadeOut.beginTime = .2;
CAAnimationGroup *both = [CAAnimationGroup animation];
both.animations = #[resize, fadeOut];
CALayer *squareLayer = nil; // Your layer code here.
[squareLayer addAnimation:both forKey:#"Do the focus animation"];
// Make sure to remove the layer after the animation completes.
Things to note are:
I'm animating bounds.size because the frame isn't really changing and it's better to be precise.
The group has the total duration
keyTimes are specified from 0 to 1
When the animation completes it will be removed from the layer.
Since the last thing in your animation is to fade the opacity to 0 you should removed it when you are done. One way of doing this is to become the delegate of the group and implement
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
// remove layer here since it should have faded to 0 opacity
}

UIPanGestureRecognizer on a UIView behaves oddly after UIViews layer is animated

I have a series of UIViews which I animate along an arc. Each view has a UIPanGestureRecognizer on it which before any animation occurs works as expected.
However after animating the views (with the method below which animates a view's layer) the gestures do not work as expected; in fact it appears as if they work in their original screen positions.
I've tried various things including matching up the views frame and/ or bounds with that on the animated layer and even removing the existing gestures and creating and adding them again (not shown below).
If anyone knows what's going on or can get me on the road to fixing the issue it would be greatly appreciated.
EDIT: iMartin's suggestion to change
pathAnimation.removedOnCompletion = NO;
to
pathAnimation.removedOnCompletion = YES;
and
view.layer.position = ... //
have put me on the way to fixing the issue.
Original Code:
- (void)animateUIViewAlongArc:(UIView*)view clockwise:(BOOL)clockwise duration:(float)duration
{
[UIView animateWithDuration:duration animations:^{
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimationanimationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.repeatCount = 1;
CGPoint viewCenter = view.objectCenter;
CGMutablePathRef arcPath = CGPathCreateMutable();
CGPathMoveToPoint(arcPath, NULL, viewCenter.x, viewCenter.y);
float angleOfRotation = self.angleIncrement;
if (clockwise == NO)
{
angleOfRotation *= -1.0f;
}
float fullwayAngle = view.angle + angleOfRotation;
CGPoint fullwayPoint = pointOnCircle(self.center, self.radius, fullwayAngle);
CGPathAddRelativeArc(arcPath, NULL, self.center.x, self.center.y, self.radius, view.angle, angleOfRotation);
pathAnimation.path = arcPath;
CGPathRelease(arcPath);
[view.layer addAnimation:pathAnimation forKey:#"arc"];
view.angle = fullwayAngle;
view.objectCenter = fullwayPoint;
} completion:^(BOOL finished){
if (finished)
{
CGRect frame = view.layer.frame;
CGRect bounds = view.layer.bounds;
view.frame = frame;
view.bounds = bounds;
}
}];
}
Did you check the frame of the view/layer after the animation?
I think the problem is, that what you see is not what you get. The layer itself didn't changes its position/frame, but just its presentationLayer did. Presentation layer is what you actualy see onthe screen. You could see this if you remove this line:
pathAnimation.removedOnCompletion = NO; // Delete or set to YES
Setting this to NO caused the presentation layer to be on screen at different position than your view.
What you should do, is to set final position to the layer just before adding the animation to it.
view.layer.position = finalPosition; // You should calculate this
[view.layer addAnimation:pathAnimation forKey:#"arc"];

Animation along path - Completion method called too early

I'd like to implement an animation along a rounded path and fire a completion function as soon as the animation has ended.
Thanks to this post I figured out how to use a CAKeyframeAnimation with a CGMutablePathRef to perform the animation along a path.
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration = 1.0;
CGPoint endPoint = CGPointMake(self.boardReference.view.bounds.size.width/2,self.boardReference.view.bounds.size.height/2);
CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, self.view.center.x, self.view.center.y);
CGPathAddCurveToPoint(curvedPath, NULL, endPoint.x, self.view.center.y, endPoint.x, self.view.center.y, endPoint.x, endPoint.y);
pathAnimation.path = curvedPath;
CGPathRelease(curvedPath);
[self.view.layer addAnimation:pathAnimation forKey:#"curvedPath"];
[self addNewCardAtPoint:endPoint]; //This should only be launched after the animation has been finished
Problem: My method addNewCardAtPoint should only be called after the animation has been completed. Currently the process is not blocking so the method is called more or less simultaneously.
At the first view the animateWithDuration function seems to better suited as it has defined blocks for the animation and the completion action. But simply adding the CAKeyframeAnimation to the animations block has basically the same result, immediate execution of the completion block.
[UIView animateWithDuration:0.8
animations:^{
NSLog(#"-Animate on curved path to end point-");
... //how perform the same path based animation here?
} completion:^(BOOL finished) {
[self addNewCardAtPoint:self.view.center];
}
];
How can I fire a complete function only after the animation along the curved path has been completed?
When a CAAnimation is done, it will call the animationDidStop:finished: method on its delegate.

CAKeyframeAnimation not animating until I rotate device

I seem to be missing the obvious when animating a key frame. I have looked at many code samples including Apple's MoveMe, referenced in the CAKeyframeAnimation documentation, yet, I cant find a discrepancy that would cause what I'm seeing.
I create a CGMutablePathRef, then a CAKeyframeAnimation and set it to animate an image view along the path. An animation group is created so I can remove the view when done.
Yet, my animation never shows up. UNTIL I rotate the device. It seems a relayout of the view causes the animation to kickstart. I tried the obvious like [theImageView setNeedsDisplay] or even setNeedsLayout, and on the container view as well. Yet, still cant get it to work when I need to. They only show up when I rotate the device.
In the following, -cgPathFromArray: takes an NSArray of internal opcodes which is converted into a CGPathRef. Its verified to be correct because when I rotate the device, the animation does show along the programmed path.
- (void) animateImage: (NSString*) imageName
onPath: (NSArray*) path
withDuration: (NSString*) duration
{
if (self.sceneView)
{
CGMutablePathRef animationPath = [self cgPathFromArray: path];
if (animationPath)
{
UIImage* image = [self findImage: imageName];
if (image)
{
UIImageView* imageView = [[UIImageView alloc] initWithFrame: CGRectMake(0, 0, image.size.width, image.size.height)];
if (imageView)
{
CAKeyframeAnimation* keyFrameAnimation = [CAKeyframeAnimation animationWithKeyPath: #"position"];
imageView.image = image;
[self.sceneView addSubview: imageView];
keyFrameAnimation.removedOnCompletion = YES;
keyFrameAnimation.fillMode = kCAFillModeForwards;
keyFrameAnimation.duration = duration.floatValue;
keyFrameAnimation.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
keyFrameAnimation.repeatCount = 0;
keyFrameAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[keyFrameAnimation setPath: animationPath];
//group animation with termination block to cleanup
CAAnimationGroup* group = [CAAnimationGroup animation];
group.duration = keyFrameAnimation.duration;
group.removedOnCompletion = YES;
group.fillMode = kCAFillModeForwards;
group.animations = #[keyFrameAnimation];
CorpsAnimationCompletionBlock theBlock = ^void(void)
{
[imageView removeFromSuperview];
};
[group setValue: theBlock
forKey: kCorpsAnimationCompletionBlock];
group.delegate = self;
[imageView.layer addAnimation: group
forKey: nil];
}
}
}
}
}
Anyone can help with this?
You are probably having trouble because you're adding an animation to a layer in the same transaction where the layer is added to the visible layer tree. Core Animation doesn't like to attach animations to layers that haven't been committed yet. You may be able to work around this by doing [CATransaction flush] after adding the layer.
Your code is rather hard to look at because of the excessive nesting. Consider using early returns to make it more readable.
Also, you're explicitly creating the same frame that the -[UIImage initWithImage:] initializer would create for you.
If you're using an animation group and setting a delegate simply so you can execute a block at the end of the animation, there is an easier way. You can begin a CATransaction, set the transaction's completion block, then add the animation, then commit the transaction.
Thus:
- (void) animateImage:(NSString *)imageName onPath: (NSArray *)path
withDuration: (NSString *)duration
{
if (!self.sceneView)
return;
CGMutablePathRef animationPath = [self cgPathFromArray:path];
if (!animationPath)
return;
UIImage *image = [self findImage:imageName];
if (!image)
return;
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[self.sceneView addSubview: imageView];
// commit the implicit transaction so we can add an animation to imageView.
[CATransaction flush];
[CATransaction begin]; {
[CATransaction setCompletionBlock:^{
[imageView removeFromSuperview];
}];
CAKeyframeAnimation *animation = [CAKeyframeAnimation
animationWithKeyPath:#"position"];
animation.duration = duration.floatValue;
animation.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
animation.path = animationPath;
[imageView.layer addAnimation:animation forKey:animation.keyPath];
} [CATransaction commit];
}

Resources