UIView animateWithDuration not running second time - objective-c

I've tried a lot of different options, and looked through about 15 stack answers and I just can not figure this out.
The code is basically trying to fade out, and then pop back, a view every time a tap happens. It works fine the first time, but will not work any subsequent times.
- (void)handleTap:(UIGestureRecognizer*)gestureRecognizer
{
self.view.transform = CGAffineTransformIdentity;
__block HelpScreenController* weakSelf = self;
[UIView animateWithDuration:10
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^(void) {
weakSelf.view.alpha = 0;
}
completion:^(BOOL finished) {
if (finished) {
weakSelf.view.alpha = 100.0f;
[weakSelf.view.layer removeAllAnimations];
[weakSelf.view setNeedsDisplay];
}
}];
}
It runs perfectly the first tap - it smoothly transitions from opaque to fully transparent over a 10 second period. Second+ tap(s) it wil sit there for 10 seconds, then go transparent for a heart beat then go back to fully opaque again.
How can I get it to animate smoothly every time?
Thanks in advance!

alpha values are between 0.0f and 1.0f. Setting the alpha value in the completion block to 1.0f instead of 100.0f should fix the problem.
Because values larger than 1.0f are all completely opaque, you will not see the transition from 100.0f to 1.0f (99% of your animation), so the effective duration of the transition from 1.0f to 0.0f would just be about 0.1 seconds instead of 10 (not exactly, because of the animation curve, but you get the idea).

You could just use a CABasicAnimation instead. Try out something like this:
- (void)handleTap:(UIGestureRecognizer*)gestureRecognizer
{
CALayer *viewLayer = self.view.layer;
[viewLayer removeAllAnimations];
CABasicAnimation *fader = [CABasicAnimation animationWithKeyPath:#"opacity"];
fader.fromValue = [NSNumber numberWithFloat:0.0];
fader.toValue = [NSNumber numberWithFloat:1.0];
fader.duration = 10;//change the duration and autoreverses option to fit with your look
fader.autoreverses = YES;
fader.repeatCount = 0;
[viewLayer addAnimation:fader forKey:#"fadeAnimation"];
}
Hope it helps!

Related

how to animate both the frame of an UIView and the frame of one of its sublayers?

What I have:
I have a UIView (named pView) which has as sublayer a CAGradientLayer. Practically is this:
ViewController -> View ->pView -> CAGradientLayer
This is the code that creates all this:
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITestView * pView = nil;
UIColor * scolor = nil, *ecolor = nil;
self.view.backgroundColor = [UIColor yellowColor];
pView = [[UITestView alloc] initWithFrame: CGRectMake(self.view.frame.origin.x, 100.0, self.view.frame.size.width, 100.0)];
scolor = [UIColor colorWithRed:(14/255.0) green: (238/255.0) blue:(123/255.0) alpha:1];
ecolor = [UIColor colorWithRed:(6/255.0) green: (216/255.0) blue:(69/255.0) alpha:1];
// creating the gradient layer
CAGradientLayer * layer = [[CAGradientLayer alloc] init];
layer.frame = self.bounds;
layer.colors = #[(id)scolor.CGColor,(id)ecolor.CGColor];
[pView.layer insertSublayer:layer atIndex:0];
// creating a tapGestureRecognizer
[pView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapInViews:)]];
[self.view addSubview:pView];
touched = NO;
}
....
#end
What I'm trying to do
Once a tap gesture is detected over pView I want to increase the height of pView by 100.0 but animatedly. If pView was previously touched (that is, its height was already increased by 100.0) I want to decrease the height of pView by 100.0 (that is, returning it to its original size);
What I already know
I know that since I want to change the frame of pView I must change also the frame (or bounds) of the CAGradientLayer attached to pView. Since I want to animate these changes, I want these animations occurs at the same time and have the same duration.
I know that the frame (or bounds) of a layer is only animatable inside an animation block.
What I do:
This is the option I've test:
- (void) handleTapInViews: (nonnull UITapGestureRecognizer *) sender {
pView = sender.view;
if (touched) {
[UIView animateWithDuration:0.4 animations:^{
pView.frame = CGRectMake(pView.frame.origin.x, pView.frame.origin.y, pView.frame.size.width, pView.frame.size.height - 100.0);
pView.layer.sublayers[0].frame = pView.bounds;
[self.view setNeedsLayout];
} completion:^(BOOL finished) {
touched = NO;
}];
}
else {
[UIView animateWithDuration:0.4 animations:^{
pView.frame = CGRectMake(pView.frame.origin.x, pView.frame.origin.y, pView.frame.size.width, pView.frame.size.height + 100.0);
pView.layer.sublayers[0].frame = pView.bounds;
[self.view setNeedsLayout];
} completion:^(BOOL finished) {
touched = YES;
}];
}
}
This option actually works (that is, both the frame of pView and the frame of the layer change animatedly) but they are not synchronised; that is, the changes in the frame of the layer are slightly (but perceptible) more faster than the changes in the frame of pView. This effect is more evident when the height of pView is decreased.
I 've also read about CABasicAnimation and CAAnimationGroup, in order to animate both the bounds and position of the layer. In this case also the animation of the layer is bit faster than the animation of the view (it is perceptible and it is not a matter of seconds in case anyone ask about setting duration or whatever) and also in this case, after the animation the bound of the layer return to its original size which is not the desired effect. I already know this last matter can be fixed assigned the new values to the layer at the end of the animation but I certainty do not know where in my code put that.
Most of what i've read regarding this other option is from these links:
link1
link2
link3
link4
In any case, does anybody please knows how can I fix this?? thanks in advance.
Well I ended setting the backgroundColor of Pview as ecolor = [UIColor colorWithRed:(6/255.0) green: (216/255.0) blue:(69/255.0) alpha:1. This way, the effect I commented about the animation of the layer been faster than the animation of the view is not noticeable now. I think maybe this is not the proper answer but it suit me.

Rotating a UIImage to point in the right direction defined by calculated angle

I have a cartoon character in a UIImage that is added as a subview to my main UIView. I take the centre coordinates of the image along with a randomly generated CGPoint and use trig to find the angle that the character needs to rotate to point in the right direction. I use UIView animation within a block. The first part does the rotate, then the completion for the first part does the move to the new location, then the completion for the inner block calls the animation loop again. This rotate/move sequence continues until the user stops it - so essentially, the character is continually moving around the screen.
It seems to be working for the first rotate/move sequence, but then it starts to face the wrong way. I've looked at the angles and they seem to be calculating correctly (I get that negative goes counter clockwise), so I'm wondering if there's something in how the translate to the new location and its associated new coordinates work? I've tried doing this with blocks, without blocks, changing to layers and using CGAffine functions, but this should be quite simple and easy to do with UIView animations.
In short, I'm stumped and need some help or pointers in the right direction. I'm probably missing something obvious so forgive me if that's the case but I've been at this for two weeks now so I'm a bit 'can't see the woods for the trees'!!
The pertinent code: I add the character (theBug), which is a custom class (that subclasses the UIImageView):
if (!doesContain){
NSLog(#"adding bug for first time");
//doesContain=YES; //set this at the end of the method after first call to animateLoop
[self.theBug setTranslatesAutoresizingMaskIntoConstraints:YES];
self.theBug=[[BugView alloc] initWithImage:[UIImage imageNamed:#"newbug1.png"]];
self.theBug.tag=3; //to identiry this in clearBoard
self.theBug.frame=CGRectMake(100, 100, 30, 30);
[self.gameView addSubview:self.theBug];
self.gameView.autoresizingMask=UIViewAutoresizingNone;
self.theBug.autoresizingMask=UIViewAutoresizingNone;
}
The I do the animation:
//Random x&y points
CGFloat x=(CGFloat) (arc4random() % (int) self.gameView.frame.size.width);
CGFloat y = (CGFloat) (arc4random() % (int) self.gameView.frame.size.height);
//create the random point to move the bug to
CGPoint rotateToLocation= CGPointMake(x, y);
//Store the bug's current location - This might not be the best way to represent this after rotation
CGPoint bugLocation = CGPointMake(self.theBug.frame.origin.x, self.theBug.frame.origin.y);
//Calculate the angle to rotate
float angleToRotate = atan2f(self.theBug.transform.tx-rotateToLocation.x, self.theBug.transform.ty - rotateToLocation.y);
if (self.theBug.transform.tx <x)
angleToRotate*=-1;
double delayInSeconds = 1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[UIView animateWithDuration:4
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
self.theBug.transform = CGAffineTransformRotate(self.theBug.transform, angleToRotate);
} completion:^(BOOL finished) {
[UIView animateWithDuration:4
delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^{
self.theBug.center = rotateToLocation;
} completion:^(BOOL finished) {
[self animationLoop:#"bug" finished:[NSNumber numberWithInt:0] context:nil];
}];
}];
});
The answer was simple! My calculations were correct, the problem was my starting image was oriented incorrectly. I rotated the original image to the right position and it all worked fine.

Alpha changing instantly with UIView animateWithDuration: instead of animating

I'm attempting to fade in a UIView that I've created by animating the alpha. I need to fade it in, leave it visible for a few seconds, then fade it out.
The fade out function works fine. The view smoothly disappears. But the fade in just makes the view appear instantly instead of slowly appearing over an interval of 0.5 seconds.
So it seems like the fade in animation isn't working, just instantly setting the alpha to 1.0. I'm kind of at a loss here. Any ideas what I'm doing wrong? Thanks!
-(void)presentPopupPhrase:(NSString *)phrase
inView:(UIView *)view
withDelegate:(id)delegate
andCompletion:(void (^)(BOOL completed))completion {
MessagePopupView *pv = [[[MessagePopupView alloc] initWithFrame:self.frame andText:phrase] autorelease];
pv.alpha = 0.0;
[view addSubview:pv];
[self fadeInMPV:pv
withDuration:self.fadeDuration
andDelay:self.fadeInDelay];
[self fadeOutMPV:pv
withDuration:self.fadeDuration
afterDelay:self.fadeOutDelay
withCompletion:completion
andDelegate:delegate];
}
-(void)fadeInMPV:(MessagePopupView *)mpv
withDuration:(NSTimeInterval)duration
andDelay:(NSTimeInterval)delay
{
[UIView animateWithDuration:duration
delay:delay
options:UIViewAnimationOptionCurveLinear
animations:^{
mpv.alpha = 1.0;
}
completion:nil];
}
-(void)fadeOutMPV:(MessagePopupView *)mpv
withDuration:(NSTimeInterval)duration
afterDelay:(NSTimeInterval)delay
withCompletion:(void (^)(BOOL completed))completion
andDelegate:(id)delegate
{
[UIView animateWithDuration:duration
delay:delay
options:UIViewAnimationOptionCurveLinear
animations:^{
mpv.alpha = 0.0;
}
completion:completion];
}
EDIT:
If it helps, here's the VC code where I'm calling it from:
-(void)viewDidAppear:(BOOL)animated {
CGRect phraseFrame = CGRectMake(20, 341, 280, 65);
PopupPhraseController *phraseController = [[[PopupPhraseController alloc] initWithFrame:phraseFrame] autorelease];
[phraseController presentPopupPhrase:#"Test Phrase" inView:self.view withDelegate:self andCompletion:^(BOOL completed){
if (completed) {
NSLog(#"completed");
} else {
NSLog(#"not completed");
}
NSLog(#"blocked!");
}];
[super viewDidAppear:animated];
}
You can't chain the animations like that as the second animation block essentially causes the first one to be cancelled out.
You have two options for Linking Multiple Animations Together:
Use the completion handler of the first animation
Nest the animations but delay the second one
Looks something like this
[UIView animateWithDuration:self.fadeDuration
delay:self.fadeInDelay
options:UIViewAnimationOptionCurveLinear
animations:^{
pv.alpha = 1.0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:self.fadeDuration
delay:self.fadeOutDelay
options:UIViewAnimationOptionCurveLinear
animations:^{
pv.alpha = 0.0;
} completion:completion];
}];
Looks something like this
[UIView animateWithDuration:self.fadeDuration
delay:self.fadeInDelay
options:UIViewAnimationOptionCurveLinear
animations:^{
pv.alpha = 1.0;
[UIView animateWithDuration:self.fadeDuration
delay:self.fadeOutDelay + self.fadeDuration
options:UIViewAnimationOptionCurveLinear
animations:^{
pv.alpha = 0.0;
} completion:completion];
} completion:nil];
The issue is because of the way you are setting up the animations.
When you set animatable properties on a view the backing model of the view is updated instantly. So when you set the alpha = 1.0 in the first block the backing data model for the view has the alpha as 1.0 and then the actual animation is kicked off on another thread to show the interpolation between the two values.
When you define the second block straight after the first the view essentially says "ok I need to animate from 1.0 (my current value in the model) to 0.0", which is why you don't see what you expected.
Thanks to Paul.s for dropping the hint there. I think this expected behaviour is a little weird, even if it seems logical from a low level perspective.
To expand on Paul's 2 solutions, there is a 3rd solution which is better IMO.
Use GCD to add a delay to the fade out code, like such:
//Make sure this delay exceeds your fade-in time...
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self fadeOutMPV:pv
withDuration:self.fadeDuration
afterDelay:self.fadeOutDelay
withCompletion:completion
andDelegate:delegate];
});
This solution's better because the executing view controller has complete control on not only when, but what gets to trigger the fade out (for example, a user action).

iOS Cross Dissolve WITHOUT a darker middle

We are trying to do a dissolve between two almost exact images (almost animation frames) but so far we cannot remove the darkening that happens midway through the dissolve. The client is very firm about not having this darker dissolve, anything thought?
UIImageView *newImageView = [[UIImageView alloc] initWithFrame:imageViewSmileDesign.frame];
newImageView.image = imageViewSmileDesign.image;
[self.view addSubview:newImageView];
imageViewSmileDesign.image = [UIImage imageNamed:[NSString stringWithFormat:#"%#.jpg", result]];
imageViewSmileDesign.alpha = 0;
[UIView animateWithDuration:0.5 animations:^{
imageViewSmileDesign.alpha = 1;
newImageView.alpha = 0;
} completion:^(BOOL finished) {
[newImageView removeFromSuperview];
} ];
Thanks for looking.
You could use the cross-disolve animation that's built into UIView:
+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion
Set options to UIViewAnimationOptionTransitionCrossDissolve.
Just don't change the alpha of 'newImageView' to 0. This will cause the new image to fade on top of the old one, and you don't have this half-way state where they are both semi-transparent revealing the background color.
You will have to bring imageViewSmileDesign to the front with a call to bringSubviewToFront: before the animation.
If both images have the same frame, just remove newImageView.alpha = 0;. That'll do the trick.
You need to delay the START of the newImageView.alpha fade.

iOS/Objective-C: transitionWithView custom animation block resizes view immediately, not over duration

The following code is a method I created inside a UIViewController to popup/down a "reader" overlay on top of the controller's own view. The intention is for the reader to begin as transparent, size zero, at a specific point. "Popup" is then animated as increasing in opacity and size, and shifts towards an application frame central position. "Popdown" is subsequently animated as the reverse, shrinking back whilst moving toward a specified location, fading out.
The popup code works exactly as desired. However, the popdown version (i.e. code executed if isPopup == NO) immediately changes the bounds rather than doing so gradually. Thus the popdown animation shows from the beginning a 1 pixel square view moving towards its destination and fading out.
-(void)popupReader:(BOOL)isPopup from:(CGPoint)loc {
CGFloat newAlpha = 0.0f;
CGPoint newCenter = CGPointZero;
CGRect newBounds = CGRectZero;
CGRect appFrame = [UIScreen mainScreen].applicationFrame;
CGSize readerSize = [self viewSize];
if (isPopup) {
newAlpha = 1.0f;
newCenter = CGPointMake(appFrame.origin.x+appFrame.size.width/2,
appFrame.origin.y+appFrame.size.height/2);
newBounds = CGRectMake(0,0,readerSize.width,readerSize.height);
[self.view setAlpha:0.0f];
[self.view setCenter:loc];
[self.view setBounds:CGRectMake(0, 0, 0, 0)];
} else {
newCenter = loc;
newBounds = CGRectMake(0,0,1,1);
}
const CGFloat animDur = 0.3f;
[UIView transitionWithView:self.view
duration:animDur
options:UIViewAnimationOptionTransitionNone|UIViewAnimationOptionCurveEaseOut
animations:^{
self.view.alpha = newAlpha;
self.view.center = newCenter;
self.view.bounds = newBounds;
}
completion:nil];
}
I've already tried animating just the frame, rather than bounds and center, but the result was identical.
Does anyone know why this is happening, and how I can overcome this problem?
Many thanks for your time.
from the docs:
UIViewAnimationOptionTransitionNone -
An option for specifying that no transition should occur.
try one of:
UIViewAnimationOptionTransitionFlipFromLeft
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp
UIViewAnimationOptionTransitionCurlDown
Edit:
Or if you're just looking to animate those values, try
[UIView animateWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations
completion:(void (^)(BOOL finished))completion];