Core Animation with Blocks on the Mac? - objective-c

Is it possible to perform animations with Core Animation using blocks on the Mac similarly to how one can do it on iOS? I'd like to be able to set up completion blocks at the end of an animation to remove views, etc. I know that this can be achieved with delegates, but obviously the whole point of blocks with animations is to avoid that pain.

CATransaction + (void)setCompletionBlock:(void (^)(void))block
The block object called when animations for this transaction group are completed.
[CATransaction begin];
[CATransaction setAnimationDuration:5.0];
[CATransaction setCompletionBlock:^{
// this will be done when animation has completed
}];
//do some things to your layers
[CATransaction commit];

Twitter is offering TwUI that uses Core Animation with Blocks. It might help you.
https://github.com/twitter/twui
https://github.com/twitter/twui/blob/master/lib/UIKit/TUIView+Animation.m
#interface TUIViewAnimation : NSObject <CAAction>
{
/* snip */
void (^animationCompletionBlock)(BOOL finished);

Sometimes. NSAnimationContext and NSAnimationGroup both have completionHandler properties to which you can assign blocks, but many others don't.

Related

Can I 'flush' UIGestureRecognizer events?

In iOS 7, Apple seems to have changed the way the gesture recognizers behave. Consider UIPinchGestureRecognizer as an example. If I do a slow redrawing operation in UIGestureRecognizerStateChanged, this used to work fine under old versions of iOS, but in newer versions, my redrawing typically doesn't get rendered to the screen before the pinch gesture is called again with another StateChanged update, and the slow drawing operation is invoked again. This happens repeatedly many times before the system actually updates the visible portion of the screen with my changes to the views.
I've found one solution is to call:
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
whenever I get a UIGestureRecognizerStateChanged event. This way drawing gets rendered on the screen, each time it's done. But there is still an issue of "event lag" where a series of pinch events gets queued up, such that the images keep scaling in size even long after I've stopped pinching the screen.
My question is if there's a way to "flush" the queued up pinch events, so whenever I get a UIGestureRecognizerStateChanged event, I can do my slow drawing operation, then flush all other pinch events, so only the most recent one gets processed. Anyone know if this is possible? I guess I could build a system that looks at the time of a UIGestureRecognizerStateChanged event, and throws out events too close to the most recent redraw, but that seems like a hack.
- (void) handleGlobalPinchGesture:(UIPinchGestureRecognizer*)_pinchGesture
{
if ( _pinchGesture.state == UIGestureRecognizerStateBegan )
{
// stuff
return;
}
if ( _pinchGesture.state == UIGestureRecognizerStateEnded || _pinchGesture.state == UIGestureRecognizerStateCancelled )
{
// end stuff
return;
}
if (_pinchGesture.state == UIGestureRecognizerStateChanged )
{
doSlowRedrawingOperationHere();
}
}
I do not think, that it is the gesture recogniser's problem, I've had same problem with moving a transformed view. And I've solved it, by add to view drawRect method, and call -(void)setNeedsDisplay method before change the centre of the view:
In view:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
}
In a gesture recogniser's action:
[CATransaction begin];
[CATransaction setValue:#(YES) forKey:kCATransactionDisableActions];
_destinationIndicatorView.center = center;
[self.frameView setNeedsDisplay];
self.frameView.center = center;
[CATransaction commit];
It works for me.
I never did find a way to 'flush' these events, but I did find a 'hack' that ensures every render is reflected on-screen, so the user sees your gesture actions in real-time, even if such redrawing operations are slow. My solution is to call:
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
to 'give the OS time' to do the redrawing on-screen. I do this from within my gesture recognizer callback, and only if running on iOS 7 or later.
The above call must be added to the callback for all your gesture recognizers (after the new content is rendered). Hope this helps someone!
Too bad that this 'hack' currently seems to be required in iOS 7 when you have slow rendering that takes place as a direct response to a gesture, if you want a good user experience.

animationDidStop in ios

I'm generating CALayer when user taps the screen. Then I'm translating that layer to certain position using Animation. Then I'm removing it using this code in animationDidStop:
[mylayer removeFromSuperLayer];
Here everything is working fine, but when I tap again before the previous animation stops, my current layer is not removed from the superlayer. How do I removed it under these circumstances?
If you are creating a new layer each time, then the delegate method will only be able to remove the current one (i.e. the older one will be lost)
You could try using CATransaction begin/commit pairs around your animations and adding completion block, this way you can pass the layers reference for each animation
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[myLayer removeFromSuperlayer];
}];
//your existing animation code
[CATransaction commit];

Checking When animation stopped

I am working on a simple UIImageView animation. Basically I got a bunch of image numbers from 0-9 and I am animating them using this code.
myAnimation.animationImages = myArray;
myAnimation.animationDuration = 0.7;
myAnimation.animationRepeatCount = 12;
[myAnimation startAnimating];
Here myArray is an array of uiimages i.e. 0.png, 1.png etc.
All is well and its animating just fine. What I need to know is when do I know when the animation has stopped? I could use NSTimer but for that I need a stopwatch to check when the animation starts and stops. Is there a better approach, meaning is there a way I can find out when the UIImageView stopped animating?
I looked at this thread as reference.
UIImageView startAnimating: How do you get it to stop on the last image in the series?
Use the animationDidStopSelector. This will fire when the animation is done:
[UIView setAnimationDidStopSelector:#selector(someMethod:finished:context:)];
then implement the selector:
- (void)someMethod:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context {
}
Yes, there is a much better approach than using NSTimers. If you're using iOS 4 or higher, it is better you start using block animations. try this
[UIView animateWithDuration:(your duration) delay:(your delay) options:UIViewAnimationCurveEaseInOut animations:^{
// here you write the animations you want
} completion:^(BOOL finished) {
//anything that should happen after the animations are over
}];
Edit: oops I think I read your question wrong. In the case of UIImageView animations, I can't think of a better way than using NSTimers or scheduled events
You can also try this:
[self performSelector:#selector(animationDone) withObject:nil afterDelay:2.0];

CATransaction duration not working

I set a few CALayer transform and bounds modifications, within a CATransaction. However, regardless the method I use (key-value, setAnimationDuration) there is no animation, the changes are done, but immediately without transition.
Do you have any idea why?
Thanks!
/* CALayer*layer=[CALayer layer];
layer.bounds =AnUIImageView.bounds;
layer.contents=AnUIImageView.layer.contents;
[AnotherUIImageView.layer addSublayer:layer];
CGPoint thecentre=AnUIImageView.center;
CALayer* layerInTarget=[AnotherUIImageView.layer.sublayers lastObject];
[layerInTarget setPosition:[self.view convertPoint:thecentre toView:AnotherUIImageView]];
AnUIImageView.layer.hidden=YES;
*/ // the code above works, i show it to be complete
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.0f]
forKey:kCATransactionAnimationDuration];
layerInTarget.position=[self.view convertPoint:AnotherUIImageView.center toView:AnotherUIImageView];
layerInTarget.transform=CATransform3DMakeScale(0.6,0.6,0.6);
[CATransaction commit];
CALayers that are associated with a UIView (as in, they're accessed via view.layer) do not participate in implicit animations, regardless of how you configure your CATransaction. You either need to use explicit animations (using the appropriate subclass of CAAnimation) or you need to use UIView animations.

Consecutive animations using nested animation blocks

I'm looking for a way to implement consecutive animations using nested animation blocks.
Somewhat complicated by happening inside a UIScrollView, the size of three UIImageViews (there are many images, and as I scroll through them I constantly swapping out the images in the UIImageViews).
When a scroll is finished, I want to switch out the image in the (visible) middle UIImageView, three times, then back to the original view. I'm trying it thus:
- (void) doAnimation {
// get the animation frames, along with the current image
NSString *swap1 = #"first.png";
NSString *swap2 = #"second.png";
UIImage *original = currentPage.image;
UIViewAnimationOptions myOptions = UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction;
[UIView animateWithDuration:2.0 delay:2.0 options:myOptions
animations:^{ [currentPage setImage:[UIImage imageNamed:swap1]]; }
completion:^(BOOL finished) {
[UIView animateWithDuration:2.0 delay:2.0 options:myOptions
animations:^{ [currentPage setImage:[UIImage imageNamed:swap2]]; }
completion:^(BOOL finished) {
[UIView animateWithDuration:2.0 delay:2.0 options:myOptions
animations:^{ [currentPage setImage:[UIImage imageNamed:swap1]]; }
completion:^(BOOL finished) {
[currentPage setImage:original]; }]; }]; }];
}
When I run this, there is no duration, no delay, it all happens at once, almost too fast for the eye to see. Could this be because "currentPage" is a UIImageView? (Similar to this question?)
There's no delay because UIImageView.image isn't an animateable property. As such, the UIView animation machinery will have no animations to set up and will just call your completion block immediately.
What sort of animation did you expect? You can attach a CATransition object to the underlying layer to get a simple cross-fade, Just use [imageView.layer addAnimation:[CATransition animation] forKey:nil] to get the crossfade with the default values (you can customize the timing by modifying properties of the CATransition before attaching it to the layer). To achieve the subsequent animations, you can either use the delegate property of CAAnimation (CATransition's superclass) to learn when it's done and fire your second one, or you could just use -performSelector:withObject:afterDelay: to start your next animation step after a user-defined delay. The delegate method is going to be more accurate with regards to timing, but the performSelector method is a bit easier to write. Sadly, CAAnimation doesn't support a completion block.
Another approach for you to transition from one image view to another is by using the block animation function transitionFromView:toView:duration:options:completion as discussed in "Creating Animated Transitions Between Views". You would do this instead of animateWithDuration to change images.