performSelector:withObject:afterDelay method not working properly? - iphone

I have an app with a small uiimageview circle that fades in and out into two different locations. When the user touches the screen I want the circle to fade out immediately, and i want it to stop fading in and out into moving locations. So basically touching the screen kills the circle.
After the circle is killed though I want another circle to spawn and do the same thing (fading in and out into 2 different locations) until the user touches the screen, then i want that circle killed, and another circle to spawn and etc...
here's my simplified code:
- (void)spawnCircle {
self.circle = [[UIImageView alloc]initWithFrame:self.rectCircle];//alocate it and give it its first frame
self.circle.image=[UIImage imageNamed:#"circle.png"];//make its image the circle image
[self.view addSubView:self.circle];
[self performSelector:#selector(fadeCircleOut)withObject:self afterDelay:2];//the circle will fade out after 2 seconds
self.isFadeCircleOutNecessary=YES;
}
- (void)fadeCircleOut {
if (self.isFadeCircleOutNecessary){//because after the circle fades in this method is scheduled to occur after 2 seconds, well, if the user has touched the screen within that time frame we obviously don't want this method to be called because we already are fading it out
[UIView animateWithDuration:.5 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{//the circle will fade out for a duration of .5 seconds
self.circle.alpha=0;
} completion:^(BOOL finished) {
if (finished) {
if (self.circle.frame.origin.x==self.rectCircle.origin.x) {//if its in the first location go to the second
self.circle.frame=self.rectCircle2;
}else{//if its in the second location go to the first
self.circle.frame=self.rectCircle;
}
[self fadeCircleIn];//now were going to immediately fade it in its new location
}
}];
}
}
- (void)fadeCircleIn {
[UIView animateWithDuration:.5 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{//fade it in with a duration of .5 seconds
self.circle.alpha=1;
} completion:^(BOOL finished) {
if (finished) {
[self performSelector:#selector(fadeCircleOut) withObject:self afterDelay:2];//after 2 seconds the object will fade out again
}
}];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[UIView animateWithDuration:.5 delay:0 options:UIViewAnimationCurveLinear|UIViewAnimationOptionBeginFromCurrentState animations:^(){ self.circle.alpha=0;} completion:^(BOOL completion){//begin from current state makes it stop any animations it is currently in the middle of
[self spawnCircle];//now another circle will pop up
self.isFadeCircleOutNecessary=NO;
}];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self spawnCircle];
}
so the first time the circle spawns this works great but when the next circle spawns (after the user touches the screen and kills the first circle) the fade out method doesn't happen exactly after 2 seconds, it varies when it happens but it usually almost instantly fades out. so the delay part of the performSelector:withObject:afterDelay doesn't seem to work properly

I tried out your code, and got the same kind of results you did. I'm not sure what goes on behind the scenes with performSelector:withObject:afterDelay:, but it seems like once you get one going, it may still perform its function, even though you've created another circle.
I changed the code slightly by getting rid of the afterDelay calls, but putting the 2 second delay in the fade out method instead. See if this does what you want:
-(void)spawnCircle{
self.circle=[[UIImageView alloc]initWithFrame:self.rectCircle];//alocate it and give it its first frame
self.circle.image=[UIImage imageNamed:#"circle.png"];//make its image the circle image
[self.view addSubview:self.circle];
[self fadeCircleOut];
}
- (void)fadeCircleOut {
[UIView animateWithDuration:.5 delay:2 options:UIViewAnimationOptionCurveLinear animations:^{//the circle will fade out for a duration of .5 seconds
self.circle.alpha=0;
} completion:^(BOOL finished) {
if (finished) {
if (self.circle.frame.origin.x==self.rectCircle.origin.x) {//if its in the first location go to the second
self.circle.frame=self.rectCircle2;
}else{//if its in the second location go to the first
self.circle.frame=self.rectCircle;
}
[self fadeCircleIn];//now were going to immediately fade it in its new location
}
}];
}
- (void)fadeCircleIn {
[UIView animateWithDuration:.5 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{//fade it in with a duration of .5 seconds
self.circle.alpha=1;
} completion:^(BOOL finished) {
if (finished) {
[self fadeCircleOut];
}
}];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[UIView animateWithDuration:.5 delay:0 options:UIViewAnimationCurveLinear|UIViewAnimationOptionBeginFromCurrentState animations:^{
self.circle.alpha=0;
}
completion:^(BOOL completion){//begin from current state makes it stop any animations it is currently in the middle of
[self.circle removeFromSuperview];
self.circle = nil;
[self spawnCircle];
}];
}

Related

Interrupt completion block based animations to perform separate animations

I am using block based animations to simulate dealing cards as an intro animation for a game. The animation works great unless the user causes a segue to fire DURING the animation, where we perform additional animations in order to get the effect a "sliding" transition from source to destination view controller. What happens now is the cards that have already been "dealt" slide off screen appropriately, and if there are cards that have not been dealt, it deals it in the middle of the transition and then the card disappears when the destination view controller is pushed. It's very ugly.
I have tried 'view.layer removeAllAnimations' which didn't help (I did import quartzcore). What I want to do is cancel the pending animations in the completion blocks, and simply perform the segue animations.
Here's the "dealing" code:
[UIView animateWithDuration:0.20f
delay:0.20f
options:UIViewAnimationOptionCurveEaseOut
animations:^
{
_fiveOfHearts.center = CGPointMake(90, 198);
_fiveOfHearts.transform = fiveOfHeartsTransform;
}
completion:^(BOOL finished)
{[UIView transitionWithView:_fiveOfHearts duration:0.20f options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
_fiveOfHearts.image = [UIImage imageNamed:#"52"];
}completion:nil];
[UIView animateWithDuration:0.30f
delay:0.0f
options:UIViewAnimationOptionCurveEaseOut
animations:^
{
_jackOfHearts.center = CGPointMake(128, 196);
_jackOfHearts.transform = jackfHeartsTransform;
}
completion:^(BOOL finished)
{[UIView transitionWithView:_jackOfHearts duration:0.40f options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
_jackOfHearts.image = [UIImage imageNamed:#"112"];
}completion:nil];
[UIView animateWithDuration:0.30f
delay:0.0f
options:UIViewAnimationOptionCurveEaseOut
animations:^
{
_aceOfHearts.center = CGPointMake(162, 196);
_aceOfHearts.transform = aceOfHeartsTransform;
}
completion: ... and so on.
The segue code looks something like:
for (UIView *iv in src.view.subviews) {
if (iv.tag != 99999) {
[UIView animateWithDuration:0.5f animations:^{iv.center = CGPointMake(iv.center.x - 600, iv.center.y);}];
}
}
You could add a global BOOL say hasUserSkippedOn once the user presses to move on set it to YES and then every time you hit a completion block check if _hasUserSkipped is still YES then do not proform any more. Normally on blocks it has a default end bool but I am not too sure if animations blocks have the end bool.

Animation fails to resume through switching tabs

I've seen a bunch of similar questions on here, but none of them had what I was looking for.
Anyways:
I'm animating an Annotation on a map to move up and down. I do this by calling this function in its drawRect:
- (void) wiggle {
[UIView animateWithDuration:0.45 delay:0.0 options:UIViewAnimationCurveEaseInOut animations:^{
[self.subView setFrame:CGRectMake(self.subView.frame.origin.x, self.subView.frame.origin.y+WIGGLE_DISTANCE, self.subView.frame.size.width, self.subView.frame.size.height)];
} completion:^(BOOL finished) {
if (finished)
[UIView animateWithDuration:0.45 delay:0.0 options:UIViewAnimationCurveEaseInOut animations:^{
[self.subView setFrame:CGRectMake(self.subView.frame.origin.x, self.subView.frame.origin.y-WIGGLE_DISTANCE, self.subView.frame.size.width, self.subView.frame.size.height)];
} completion:^(BOOL finished) {if (finished) [self wiggle]; }];
}];
}
In short, it's animating a 'subView' up, and when that finishes, animating down, and when that finishes, calls itself (to continue wiggling).
This works fine until I switch tabs. When I return to this tab, there is no animation. I've tried calling 'setNeedsDisplay' on each of these views in the viewController's 'viewDidAppear', and that fails to call drawRect. So then I tried just calling 'wiggle' directly from the viewController's 'viewDidAppear', and the function DOES get called, but it just doesn't animate. Any ideas?
Thanks!

Staggered iOS animation

I would like to perform a simple animation on a series of UIImageViews in my app. I have a grid of images that I want to fade in. I can make it so that the next image starts its animation after the previous one completes, like so:
|--------animation1--------|
|--------animation2--------|
|--------animation3--------|
Which I accomplish with code like this:
- (void)loadOtherIcon;
{
UIImageView *icon = [icons objectAtIndex:iconCount];
[UIView animateWithDuration:0.5 delay:0.0 options:nil animations:^{
icon.alpha = 1;
} completion:^(BOOL finished){
if (iconCount < icons.count - 1) {
[self loadOtherIcon];
}
}];
}
But I was hoping to stagger them, like this:
|--------animation1--------|
|--------animation2--------|
|--------animation3--------|
Any suggestions?
Start each of the animations at the same time, but use an increasing "delay" amount. Currently, you're setting it to 0.0.

How to have a handler to repeat UIView animateWithDuration?

I'm using UIView class method animateWithDuration for repeating my view animation. How can I have a handler that could be used to stop this animation later? For example, repeated animation starts in one method and I need to stop it later from another method.
You could do something like this assuming you have created a canceled property. As noted in the comments the completion block's startAnimation call needs to be wrapped in an async call to avoid a stack overflow. Be sure to replace the "id" with whatever class type you actually have.
- (void)startAnimation {
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction
animations:^(void) {
//animate
}
completion:^(BOOL finished) {
if(!self.canceled) {
__weak id weakSelf = self;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[weakSelf startAnimation];
}];
}
}
];
}
The purpose of the animation is to repeatedly animate the bounce of an image. When there is no worry about manually stopping it then you just need to set three properties (UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat) and animation block code for moving the image - self.image.center = CGPointMake(self.image.center.x, self.image.center.y+25); Here is the full code of the animation:
[UIView animateWithDuration:0.5 delay:0 options:( UIViewAnimationOptionCurveEaseIn |
UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat |
UIViewAnimationOptionAllowUserInteraction) animations:^{self.image.center =
CGPointMake(self.image.center.x, self.image.center.y+25);} completion:nil];
That's it. But if you need a manual control then some additional code is required. First, according to jaminguy, you need to have a BOOL property for indication loop/stop (self.playAnimationForImage) the animation and clean separate method with animation code that would be called from elsewhere. Here is the method:
-(void)animateImageBounce{
[UIView animateWithDuration:0.5 delay:0 options:(
UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionAutoreverse |
UIViewAnimationOptionAllowUserInteraction) animations:^{self.image.center =
CGPointMake(self.image.center.x, self.image.center.y+25);} completion:^(BOOL finished){if
(finished && self.playAnimationForImage){
self.image.center = CGPointMake(self.image.center.x, self.image.center.y-25);
[self animateImageBounce];
}}];
and here is the start of the animation call from some method
-(void)someMethod{
...
self.playAnimationForFingers = YES;
[self animateImageBounce];
}
The thing that I would like to note is that, in manual control, you need to reset the center.y of the image back right before next recursive call is performed.
Actually, the solution with recursive call didn't worked out for me. The animation started to behave weirdly: every 2- 3 animatation repeat cycle I got animation breaks. After the first bouncing part of item (moving item down) the second part (moving up) was performing almost instantly. I thing it has something to do with the recursive call.
Therefore, I refused to use that. The solution would be to start the animation with autoreverse and repeat options and, in complete block, to check if a flag (self.playAnimationForFingers) indicates to stop the animation.
-(void)animateFingersForLevelCompleteScreen{
//fix: reset the place of bouncing fingers (starting place).
//Otherwise the fingers will slowly move to the bottom at every level.
//This resetting is not working when placed inside UIView
//animate complete event block
self.image.center = CGPointMake(10 + self.image.frame.size.width/2,
95 + self.image.frame.size.height/2);
[UIView animateWithDuration:0.5 delay:0
options:(UIViewAnimationOptionCurveEaseIn |
UIViewAnimationOptionAutoreverse |
UIViewAnimationOptionRepeat |
UIViewAnimationOptionAllowUserInteraction)
animations:^{
self.image.center = CGPointMake(self.image.center.x,
self.image.center.y+25);
}
completion:^(BOOL finished){
/*finished not approapriate: finished will not be TRUE if animation
was interrupted. It is very likely to happen because animation repeats && */
if (!self.playAnimationForFingers){
[UIView setAnimationRepeatAutoreverses:NO];
}
}];
}
U can make use of CABasicAnimation instead.
CABasicAnimation *appDeleteShakeAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
appDeleteShakeAnimation.autoreverses = YES;
appDeleteShakeAnimation.repeatDuration = HUGE_VALF;
appDeleteShakeAnimation.duration = 0.2;
appDeleteShakeAnimation.fromValue = [NSNumber numberWithFloat:-degreeToRadian(5)];
appDeleteShakeAnimation.toValue=[NSNumber numberWithFloat:degreeToRadian(5)];
[self.layer addAnimation:appDeleteShakeAnimation forKey:#"appDeleteShakeAnimation"];
Then when u want to stop it you can just call
[self.layer removeAnimationForKey:#"appDeleteShakeAnimation"];

our fade animation freezes button & slider controls

I have a simple photo transition on an iPad program that precludes operator interaction during the transition. The 2 photos are approx 200kb each and the fade is by changing their alphas with the following code;
-(void)photoLoopAnimation
{
// FADE block animation method - THIS contains the basic animation features
[UIView animateWithDuration:5.2 delay:delaySeconds
options:UIViewAnimationOptionTransitionNone
animations:^{
viewTop.alpha = 0.0;
viewBottom.alpha = 1.0;
}
completion:^(BOOL finished) { // remove both views & loop
[viewTop removeFromSuperview];
[viewBottom removeFromSuperview];
[viewTop release];
[viewBottom release];
[self photoLoop];
//[self performSelectorInBackground:(#selector(photoLoop)) withObject:nil];
}];
}
My understanding is that the animation block works as a background thread (task?), & shouldn't lock control of the app while it is executing. Can anyone offer any incite? (insight)
Thanks,
CPL
Try adding the UIViewAnimationOptionAllowUserInteraction option if your controls are being animated.