How do I trigger a callback when a NSAnimationContext ends? - objective-c

I have an animation which moves some views around. When this animation completes I want the window to recalculate the keyview loop. My code is simmilar to the follow mock code:
[NSAnimationContext beginGrouping];
[newView setAlpha: 0.0]; //hide newView
[self addSubView:newView];
//position the views
[[oldView animator] setFrame: newFrame1];
[[newView animator] setFrame: newFrame2];
[[newView animator] setAlpha: 1.0]; //fade-in newView
[NSAnimationContext endGrouping];
[[self window] recalculateKeyViewLoop];
The problem with this code is that recalculateKeyViewLoop is called before the views are in their new positions which means that the keyviewloop is wrong.
How do I fix this?
My first though is to call recalculateKeyViewLoop in a callback from when the animation ends but I can't figure out how to do this.

Something that's not so obvious, or at least wasn't to me, is that there are two animations going on when you do a setFrame:, with keys frameSize and frameOrigin.
Depending on what your original and final frames are you may need to register yourself as a delegate for one or both of them.
I'd also recommend that you make a copy of the animation you get back from -animationForKey: and store your modified copy in the animations dictionary of your object. This way your delegate will only be called at the conclusion of that particular objects' animator duration, versus all objects animating that key.
eg.
CAAnimation *animation = [[view animationForKey:#"frameOrigin"] copy];
animation.delegate = self;
[view setAnimations:[NSDictionary dictionaryWithObject:animation forKey:#"frameOrigin"]];
In this way your animation object will supersede the default animation object for that view. Then implement whichever delegate methods you're interested in.

You should be able to send -animationForKey: to your views to get a CAAnimation instance, then set yourself as its delegate and implement the method that Adam mentioned.

if you use CAAnimation this has an animationDidStop:finished: delegate method..
http://developer.apple.com/documentation/GraphicsImaging/Reference/CAAnimation_class/Introduction/Introduction.html#//apple_ref/occ/cl/CAAnimation
Hope this helps

You can use a completion handler like this:
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
// Start some animations.
[[myView animator] setFrameSize:newViewSize];
[[myWindow animator] setFrame:newWindowFrame display:YES];
} completionHandler:^{
// This block will be invoked when all of the animations started above have completed or been cancelled.
NSLog(#"All done!");
}];

Check out my post here: How would I do this iOS animation on OSX?
I wrote a class that handles this for you, using blocks. Hopefully your target allows blocks! :)

If you are animating an NSWindow (as opposed to an NSView), the other answers will not work and you should do this instead:
CAAnimation *animation = [CABasicAnimation animation];
animation.delegate = self;
self.window.animations = #{#"frame": animation};
[[self.window animator] setFrame:NSMakeRect(0, 0, 400, 200) display:YES];
which will then call the animationDidStop:finished: method on your delegate (which in the above code would be self)

Related

Mavericks issue: CAKeyframeAnimation on window does not animate

I am working on adapting my app to Mavericks since I have been developing it for 10.8. So far I have found various issues which I can't seem to be able to solve. One of them has to do with NSPopover-like animations.
I have a window which I animate in this way:
_zoomWindow.alphaValue = 0;
[_zoomWindow orderFront: self];
// Configure bouncing animation
CAKeyframeAnimation* frameAnim = [CAKeyframeAnimation animation];
[frameAnim setTimingFunction: [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut]];
[frameAnim setValues: #[[NSValue valueWithRect: startFrame], [NSValue valueWithRect: overshootFrame], [NSValue valueWithRect: endFrame]]];
[frameAnim setDuration: duration];
[frameAnim setDelegate: self];
[_zoomWindow setAnimations: #{#"frame": frameAnim}];
// Configure alpha animation
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration: duration];
[[_zoomWindow animator] setAlphaValue: 1.0];
[[_zoomWindow animator] setFrame: endFrame display: YES];
[NSAnimationContext endGrouping];
This works beautifully on 10.8! But it just doesn't do anything on 10.9. Am I missing something here?
I figured it out. The result was that it didn't show anything while suposedly animating, so at first I thought it wasn't animating at all. As it turns out, the problem was in another part of my code completely: the window that I was animating had the content view subclassed by me, and that view was doing some custom drawing. The real problem was that the view wasn't drawing anything on 10.9, even though it was drawing on 10.8... Once I fixed the drawing issue, the animation worked perfectly.

Alternative to "self" in calling methods in Objective-C

This may sound really noob, but i've spent an entire day wrestling with this problem and would appreciate some help.
You see i have a method which I call more than once inside gameplay. If I use [self myMethod]; then it works for ONE time. And then when I call it again, the animation in the method doesn't commence anymore.
What I need is to replace "self" with an alternative that can be "alloc'ed" and "released" to make my animations work.
I've tried;
#implementation gameViewController
gameViewController *object = [[gameViewController alloc] init];
[object myMethod];
However the above substitute for self doesn't even call on the method. I don't know what I did wrong, it's suppose to work just like "self".
Is there something i missed? How do you make an object of the class to work just like "self" does?
Thanks so much.
Here is a more detailed look of my code;
[self explosionAnimations];
- (void) explosionAnimations
{
UIImage *image = [UIImage imageNamed: #"Yellow Explosion.png"];
[bomb setImage:image];
[UIView beginAnimations:#"bomb1ExplosionIncrease" context:NULL];
[UIView setAnimationDuration:0.5];
[UIView setAnimationDelegate:self];
bomb.transform = CGAffineTransformMakeScale(3.4, 3.4);
[UIView commitAnimations];
}
The setImage works fine every time. But the animations stop working on the second call of the method. While in the console it logs "animation completed" with nothing happening to the image.
This leads me to believe that somehow "self" believes that the animation was already done and will not bother to do it again. So I thought a new "alloc" might give it a kick awake.
The problem doesn't have anything to do with "self". The problem is that you set the transform in your animation, and then when you run it again, you're setting the same transform, so it does nothing. You need to reset the frame to the new frame and then set the transform back to the identity transform before you do the animation again. Also, you should be using block based animations. I'm not sure this is the best way to do it, but this worked for me (if you have auto layout turned off).
- (void)explosionAnimations {
UIImage *image = [UIImage imageNamed: #"Yellow Explosion.png"];
[self.bomb setImage:image];
[UIView animateWithDuration:.5 animations:^{
self.bomb.transform = CGAffineTransformMakeScale(3.4, 3.4);
} completion:^(BOOL finished) {
CGRect newFrame = self.bomb.frame;
self.bomb.transform = CGAffineTransformIdentity;
self.bomb.frame = newFrame;
}];
}
If you're doing this in an app with auto layout turned on (which it is by default), then I would not use a transform, but just resize the width and height of the image view by adjusting its height and width constraints. So, in this method, you should make IBOutlets to height and width constraints you make in IB, then change their constant values in an animation block:
[UIView animateWithDuration:.5 animations:^{
self.heightCon.constant = self.heightCon.constant * 3.4;
self.widthCon.constant = self.widthCon.constant * 3.4;
[self.view layoutIfNeeded];
}];

CABasicAnimation move frame left by 300px

I need help with CABasicAnimation. I am trying to move a NSView left by 300 pixels. I found this SO thread: How to animate the frame of an layer with CABasicAnimation?
Turns out animating the frame is not possible and one of the answer points to a link to QA on Apple's website but it takes me a to a generic page:
http://developer.apple.com/library/mac/#qa/qa1620/_index.html
So, how can I do something as simple as translation of my NSView/CALyer?
Thanks!
NSView has a protocol called NSAnimatablePropertyContainer which allows you to create basic animations for views:
The NSAnimatablePropertyContainer protocol defines a way to add
animation to an existing class with a minimum of API impact ...
Sending of key-value-coding compliant "set" messages to the proxy will
trigger animation for automatically animated properties of its target
object.
The NSAnimatablePropertyContainer protocol can be found here
I recently used this technique to change the origin of a frame:
-(void)setOrigin:(NSPoint)aPoint {
[[self animator] setFrameOrigin:aPoint];
}
Instead of calling the [view setFrameOrigin:], I created another method called setOrigin: which then applies the setFrameOrigin: call to the view's animator.
If you need to change the duration of the animation, you can do so like this (similar to CATransactions):
-(void)setOrigin:(NSPoint)aPoint {
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setCompletionHandler:^{
...Completion Callback Code goes here...
}];
[[NSAnimationContext currentContext] setDuration:1.0];
[[self animator] setFrameOrigin:aPoint];
[NSAnimationContext endGrouping];
}
The NSAnimationContext is described here
You can animate center property instead. Eg:
//assuming view is your NSView
CGPoint newCenter = CGPointMake(view.center.x - 300, view.center.y);
CABasicAnimation *animation = [CABasicAnimation animation];
//setup your animation eg. duration/other options
animation.fromValue = [NSValue valueWithCGPoint:v.center];
animation.toValue = [NSValue valueWithCGPoint:newCenter];
[view.layer addAnimation:animation forKey:#"key"];

Replacing a NSSplitView subview with Core Animation

I'm trying to figure out how to animate switching (replacing) subviews in a vertically-configured, 2-view NSSplitView. I've got it semi-working using the following methods in my NSSplitView subclass:
To set up the animation:
- (void)awakeFromNib {
// set delegate
[self setWantsLayer:YES];
CATransition *transition = [CATransition animation];
[transition setType:kCATransitionPush];
[transition setSubtype:kCATransitionFromBottom];
[transition setDuration:1.0];
[self setAnimations:[NSDictionary dictionaryWithObject:transition
forKey:#"subviews"]];
}
And to perform it:
- (void)replaceRightView:(NSView *)newView animated:(BOOL)animate {
NSRect currentSize = [[[self subviews] objectAtIndex:1] frame];
[newView setFrame:currentSize];
if (animate) {
[[self animator] replaceSubview:[[self subviews] objectAtIndex:1]
with:newView];
} else {
[self replaceSubview:[[self subviews] objectAtIndex:1]
with:newView];
}
}
However, this code has the effect of pushing the entire NSSplitView off, rather than just the subview on the right side of the split.
Is there a way to animate just the subview transition? Perhaps I'm using the wrong animation key ("subviews")? Other animation methods would be fine too.
Thanks!
It's probably not the cleanest way, but I ended up using an NSView 'container' subclass with custom addSubview: and replaceSubview:withView: methods that modify the new subview's frame to match the container view's frame, which is then structured into the NSSplitView subview I wanted to animate. I then setup the CATransition on the container view, and everything worked as I wanted.

OS X version of bringSubviewToFront:?

I need to replicate the function of bringSubviewToFront: on the iPhone, but I am programming on the Mac. How can this be done?
Haven't actually tried this out - and there may be better ways to do it - but this should work:
NSView* superview = [view superview];
[view removeFromSuperview];
[superview addSubview:view];
This will move 'view' to the front of its siblings
Pete Rossi's answer didn't work for me because I needed to pop the the view to front when dragging it with the mouse. However, along the same lines the following did work without killing the mouse:
CALayer* superlayer = [[view layer] superlayer];
[[view layer] removeFromSuperlayer];
[superlayer addSublayer:[view layer]];
Also, the following placed in a NSView subclass or category is pretty handy:
- (void) bringToFront {
CALayer* superlayer = [[self layer] superlayer];
[[self layer] removeFromSuperlayer];
[superlayer addSublayer:[self layer]];
}
also be sure to enable the layer for quartz rendering:
...
NSImageView *anImage = [[NSImageView alloc] initWithFrame:NSRectMake(0,0,512,512)];
[anImageView setWantsLayer:YES];
...
otherwise, your layer cannot be rendered correctly.
Pete Rossi's answer works, but remember to retain the view when you remove it from the superview.
You can add this in a category on NSView :
-(void)bringSubviewToFront:(NSView*)view
{
[view retain];
[view removeFromSuperview];
[self addSubview:view];
[view release];
}
Sibling views that overlap can be hard to make work right in AppKitā€”it was completely unsupported for a long time. Consider making them CALayers instead. As a bonus, you may be able to reuse this code in your iOS version.
This is Swift 3.0 solution:
extension NSView {
public func bringToFront() {
let superlayer = self.layer?.superlayer
self.layer?.removeFromSuperlayer()
superlayer?.addSublayer(self.layer!)
}
}