I animate 2 views, each one with its CAAnimationGroup that contains 2 CAAnimations. They are launched at the same time (if computing time is negligible), and have the same duration.
How may I do to know when both grouped animation is finished.
I put the - (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag delegate method, but... What may I test ? Sounds simple, but I don't see the way of doing this.
You can use two variables to track whether the animations have completed:
BOOL firstAnimFinished;
BOOL secondAnimFinished;
then in your animationDidStop delegate you check which animation is calling the method and set the flags appropriately. The catch is that you'll need to add a key to identify the animations when they call the delegate (the animations you created will not be the ones calling the delegate, which is a subject for another question/rant). For example:
// when you create the animations
[firstAnmim setValue: #"FirstAnim" ForKey: #"Name"];
[secondAnmim setValue: #"SecondAnim" ForKey: #"Name"];
// Your delegate
- (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)flag {
NSString* name = [theAnimation valueForKey: #"Name"];
if ([name isEqualToString: #"FirstAnim"]) {
firstAnimFinished = YES;
} else if ([name isEqualToString: #"SecondAnim"]) {
secondAnimFinished = YES;
}
if (firstAnimFinished && secondAnimFinished) {
// ALL DONE...
}
}
Related
I have a group animation but I can not detect when hits animationDidStop. example of my code:
[group setDelegate:self];
[view.layer addAnimation:group forKey:#"groupAnimation"];
any of you knows how I know when the group animation is done?
You need to also set the animationName property to match, and ensure that your delegate function is properly defined:
CAAnimationGroup *group = [CAAnimationGroup animation];
group.duration = 2.0f;
group.delegate = self;
[group setValue:#"groupAnimation" forKey:#"animationName"];
[group setAnimations:[NSArray arrayWithObjects:myAnimation, myOtherAnimation, nil]];
[view.layer addAnimation:group forKey:#"groupAnimation"];
.
.
.
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished
{
if (finished)
{
NSString *animationName = [animation valueForKey:#"animationName"];
if ([animationName isEqualToString:#"groupAnimation"])
{
// your groupAnimation has ended
}
}
}
Please note that with group animations, the delegates set on your component animations will be ignored.
I came up with a way to organize animation completion code that works really well. First, I defined a custom type for a block of code to run when an animation completes:
typedef void (^animationCompletionBlock)(void);
I define a constant that I use to add a custom key-value pair to my animation:
#define kAnimationCompletionBlock #"animationCompletionBlock"
Then, when I create an animation, if I want it to execute a block of code when it finishes, I add a custom property to my animation that contains the block of code I want to execute:
animationCompletionBlock theBlock;
theBlock = ^void(void)
{
someButton.enabled = TRUE;
NSLog(#"Animation complete");
//whatever other code you want to do on completion
}
[myAnimation setValue: theBlock forKey: kAnimationCompletionBlock];
I set the view controller to be the animation's delegate:
myAnimation.delegate = self
Finally, I write a general-purpose animation completion method that looks for an animation completion block attached to the animation, and executes it if it finds it:
/*
This method looks for a value added to the animation that just completed
with the key kAnimationCompletionBlock.
If it exists, it assumes it is a code block of type animationCompletionBlock,
and executes the code block.
This allows you to add a custom block of completion code to any animation or
animation group, rather than Having a big complicated switch statement in your
animationDidStop:finished: method with global animation ompletion code.
(Note that the system won't call the animationDidStop:finished method for
individual animations in an animation group - it will only call the
completion method for the entire group. Thus, if you want to run code after
part of an animation group completes, you have to set up a manual timer.)
*/
- (void)animationDidStop:(CAAnimation *)theAnimation
finished:(BOOL)flag
{
animationCompletionBlock theBlock =
[theAnimation valueForKey: kAnimationCompletionBlock];
if (theBlock)
theBlock();
}
In addition to being very clean, this approach also lets your animation completion code have access to local variables that are inside the scope where you define the block. That solves the problem of passing information to your completion method, which can be difficult.
You can see this technique in a working example program I posted to Github:
Core Animation demo on Github, including completion block code
I'm using this category for setting the completion like this:
[group setCompletionBlock:^{
}];
First CAAnimationGroup+Blocks.h:
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
typedef void (^TIFAnimationGroupCompletionBlock)();
#interface CAAnimationGroup (Blocks)
- (void)setCompletionBlock:(TIFAnimationGroupCompletionBlock)handler;
#end
And CAAnimationGroup+Blocks.m:
#import "CAAnimationGroup+Blocks.h"
static char CAAnimationGroupBlockKey;
#implementation CAAnimationGroup (Blocks)
- (void)setCompletionBlock:(TIFAnimationGroupCompletionBlock)handler {
objc_setAssociatedObject(self, &CAAnimationGroupBlockKey, handler, OBJC_ASSOCIATION_COPY_NONATOMIC);
self.delegate = self;
}
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished
{
if (finished)
{
TIFAnimationGroupCompletionBlock handler = (TIFAnimationGroupCompletionBlock)objc_getAssociatedObject(self, &CAAnimationGroupBlockKey);
if (handler) {
handler();
}
}
}
#end
Is there a way to set an observer on a UIImageView.image property, so I can get notified of when the property has been changed? Perhaps with NSNotification? How would I go about doing this?
I have a large number of UIImageViews, so I'll need to know which one the change occurred on as well.
How do I do this? Thanks.
This is called Key-Value Observing. Any object that is Key-Value Coding compliant can be observed, and this includes objects with properties. Have a read of this programming guide on how KVO works and how to use it. Here is a short example (disclaimer: it might not work)
- (id) init
{
self = [super init];
if (!self) return nil;
// imageView is a UIImageView
[imageView addObserver:self
forKeyPath:#"image"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
return self;
}
- (void) observeValueForKeyPath:(NSString *)path ofObject:(id) object change:(NSDictionary *) change context:(void *)context
{
// this method is used for all observations, so you need to make sure
// you are responding to the right one.
if (object == imageView && [path isEqualToString:#"image"])
{
UIImage *newImage = [change objectForKey:NSKeyValueChangeNewKey];
UIImage *oldImage = [change objectForKey:NSKeyValueChangeOldKey];
// oldImage is the image *before* the property changed
// newImage is the image *after* the property changed
}
}
In Swift, use KVO. E.g.
imageView.observe(\.image, options: [.new]) { [weak self] (object, change) in
// do something with image
}
more discussions How i detect changes image in a UIImageView in Swift iOS
How do I draw a layer without a transaction animation? For example, when I set the contents of a layer using CATransaction it works well:
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
myLayer.contents = (id)[[imagesTop objectAtIndex:Number]CGImage];
[CATransaction commit];
but I need to change contents from the delegate method [myLayer setNeedsDisplay]. Here is the code:
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
CGContextDrawImage(context, rect, Image.CGImage]);
}
CALayer implements setNeedsDisplay method:
Calling this method will cause the receiver to recache its content. This will result in the layer receiving a drawInContext: which may result in the delegate receiving either a displayLayer: or drawLayer:inContext: message.
... and displayIfNeeded:
When this message is received the layer will invoke display if it has been marked as requiring display.
If you wrap [myLayer displayIfNeeded] in a CATransaction, you can turn off implicit animation.
You can subclass CALayer and override actionForKey:,
- (id <CAAction>) actionForKey: (NSString *) key
{
if ([key isEqualToString: #"contents"])
return nil;
return [super actionForKey: key];
}
This code disables the built-in animation for the contents property.
Alternatively, you can achieve the same effect by implementing the layer's delegate method
- (id <CAAction>) actionForLayer: (CALayer *) layer forKey: (NSString *) key
{
if (layer == myLayer) {
if ([key isEqualToString: #"contents"])
return nil;
return [[layer class] defaultActionForKey: key];
}
return [layer actionForKey: key];
}
I would like to defer auto rotating the user interface until the device has settled on an orientation for a number of seconds, rather than driving the user insane and flicking willy nilly whenever they tilt the device a few degrees off axis by mistake.
the closest i can get to this (which is by no means what I want, as it locks the UI) is:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Overriden to allow any orientation.
[NSThread sleepForTimeInterval:2.0];
return YES;
}
what i would like to do is use something like this - which works in principle, by checking the console log, but i need the appropriate line of code that has been commented out.
-(void) deferredAutorotateToInterfaceOrientation:(NSTimer *) timer {
autoRotationTimer = nil;
UIInterfaceOrientation interfaceOrientation = (UIInterfaceOrientation)[timer.userInfo integerValue];
NSLog(#"switching to new orientation %d now",interfaceOrientation);
// replace this with code to induce manual orientation switch here.
//[self forceAutoRotateToInterfaceOrientation:interfaceOrientation];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Overriden to allow any orientation.
[autoRotationTimer invalidate];
autoRotationTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self
selector:#selector(deferredAutorotateToInterfaceOrientation:) userInfo:[NSNumber numberWithInt:(int)interfaceOrientation ] repeats:NO];
NSLog(#"denying autorotate, deffering switch to orientation %d by 2 seconds",interfaceOrientation);
return NO;
}
I realize there are sometimes many ways to do things, so if this approach is not the most efficient, and someone can suggest another way to do this, I am all ears. My main criteria is I want to delay the onset of autorotation, whilst keeping a responsive user interface if indeed they have only leaned to the left slightly because they are in a bus that just went around a corner etc.
EDIT: I found a solution which may not be app store friendly, however i am a few weeks away from completion, and someone may answer this in the meantime. this works calls an undocumented method. the (UIPrintInfoOrientation) typecast is just to suppress the compiler warning, and does not affect the value being passed.
-(void ) forceUIOrientationInterfaceOrientation:(UIDeviceOrientation) interfaceMode {
[(id)[UIDevice currentDevice] setOrientation:(UIPrintInfoOrientation) interfaceMode];
}
full implementation which includes re-entrance negation is as follows:
- (void)viewDidLoad {
[super viewDidLoad];
acceptNewAutoRotation = YES;
}
-(void ) forceUIOrientationInterfaceOrientation:(UIDeviceOrientation) interfaceMode {
[(id)[UIDevice currentDevice] setOrientation:(UIPrintInfoOrientation) interfaceMode];
}
-(void) deferredAutorotateToInterfaceOrientation:(NSTimer *) timer {
autoRotationTimer = nil;
acceptNewAutoRotation = YES;
[self forceUIOrientationInterfaceOrientation:[[UIDevice currentDevice] orientation]];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Overriden to allow any orientation.
[autoRotationTimer invalidate];
if (acceptNewAutoRotation) {
autoRotationTimer = nil;
acceptNewAutoRotation = NO;
return YES;
} else {
autoRotationTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self
selector:#selector(deferredAutorotateToInterfaceOrientation:) userInfo:[NSNumber numberWithInt:(int)interfaceOrientation ] repeats:NO];
return NO;
}
}
To do this with public APIs, you probably would have to forget about autorotation, and do all your own view transforms manually based on filtered (not just delayed!) accelerometer input.
I have not tested this and it may not work at all but you can try this out:
start out self.rotate = NO; then:
- (void)shouldRotateTo:(UIInteraceOrientation *)interfaceOrientation {
self.rotate = YES;
// or test interfaceOrientation and assign accordingly.
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
bool rot = self.rotate;
self.rotate = NO
[self performSelector:selector(shouldRotateTo:) withObject:[NSNumber numberWithInt:interfaceOrientation] afterDelay:2.0];
return rot;
}
Parameter interfaceOrientation is an enum (UIInteraceOrientation) so wrap it in an NSNumber when passing.
I have seen How to identify CAAnimation within the animationDidStop delegate?, this is an addition to it.
I'm unable to get this working properly. I have an animation, and I'd like to release the controller that it was run in after the end of the animation.
Example: The controller translates from right -> left then releases itself.
Defining the animation:
NSValue *end = [NSValue valueWithCGPoint:CGPointMake(800, self.view.center.y)];
NSValue *start = [NSValue valueWithCGPoint:self.view.center];
CABasicAnimation *moveAnimation;
moveAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
moveAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
moveAnimation.duration = 0.45f;
moveAnimation.fromValue = start;
moveAnimation.toValue = end;
// actually set the position
[self.view.layer setPosition:[end CGPointValue]];
moveAnimation.delegate = self;
moveAnimation.removedOnCompletion = NO;
[self.view.layer addAnimation:moveAnimation forKey:MOVING_OUT];
Inside the delegate method:
- (void) animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
CAAnimation *check = [self.view.layer animationForKey:MOVING_OUT];
if (theAnimation == check)
{
//[check release];
[self release];
}
}
If I leave this code as-is, my controller doesn't get dealloc'd (due to the retain call by the animation).
If I run [check release], I get the message sent to deallocated instance.
Does anyone know what's wrong?
Is there another way to identify a CAAnimation in the animationDidStop delegate WITHOUT specifying removedOnCompletion = NO?
EDIT:
Forgot to mention. By not specifying that removedOnCompletion = NO, animationForKey: will return NULL. Hence I'm unable to identify the animation.
Thanks!
I think the eventual reason is CAAnimation.delegate is a retain property (very strange oops!).
The header file definition is:
/* The delegate of the animation. This object is retained for the
* lifetime of the animation object. Defaults to nil. See below for the
* supported delegate methods. */
#property(retain) id delegate;
To let self get release, the animation must be removed from layer, like:
[self.view.layer removeAnimationForKey:#THE_ANIMATION_KEY];
It's not clear what your problem is here, but it may help you to know that CAAnimation instances are generic KVO containers, so you can add custom info to them:
[myAnimation setValue: #"check" forKey: #"name"];
You can then check against that:
if ([[theAnimation valueForKey: #"name"] isEqual: #"check"])
// ...
Does that help?