Repeating NSTimer causes truncated animateWithDuration on first animation - objective-c

I have a UIViewController that, depending on the frequency set by user, displays images in a animateWithDuration fade-in/fade-out every X seconds (say, 5 or 10). To manage the regularly timed calls to fade-in/out the images, I have a NSTimer that is set every time viewWillAppear is called.
Some function that does the animation, let's call it "showImageNow":
// on...
[UIView animateWithDuration:someInterval
delay:0
options:UIViewAnimationCurveEaseInOut
animations:
^{
// UI alpha = ... code here
}
// off...
completion:^(BOOL finished){
[UIView animateWithDuration:someOtherInterval
delay:yetAnotherValue
options:UIViewAnimationCurveEaseInOut
animations:
^{
// UI alpha = ... code here
}
completion:nil
];
}
];
In viewWillAppear:
if(myTimer != nil)
{
[myTimer invalidate]; // in case user changed the frequency in settings view
}
myTimer = [NSTimer scheduledTimerWithTimeInterval: [[NSUserDefaults standardUserDefaults] doubleForKey:#"userFrequency"]
target:self
selector: #selector(showImageNow:)
userInfo: nil
repeats: YES];
In viewDidAppear:
if(myTimer) { [myTimer fire]; }
While everything works as expected most of the time, the fade-out part of the first animation is cut off/stutters every time the UIViewController is re-appeared (from say, app went to background or app was in another view). The fade-in part of the animation works always, oddly enough. This is observed on a real device, not the simulator. So the fade-in/out works for every animation except the first one (the fade-out part doesn't work).
Notes:
Yes, I've tried [myTimer fire] in the viewWillAppear (instead of viewDidAppear) as well, but this causes other issues like the UIViewController's elements show up rather abruptly when user switches to that view from other views or from background mode.
The frequency is much longer than the animateWithDuration's animation values, so there shouldn't be any frame overlaps or whatever UI overlaps there may be.
I put debug code before every animateWithDuration call in the UIVIewController, so I know for certain that no other animateWithDuration is interrupting the very first image animateWithDuration call.
So this is perplexing. I've tried using CADisplayLink but apparently that's not the right way to do it. Any ideas how to resolve this issue?

I'd try enabling the UIViewAnimationOptionBeginFromCurrentState option in your animation code and see if that helps

Related

no animation when using modal segue

I have a view controller and have a few objects that I have some simply animations hooked up to.
[UIView animateWithDuration:0.25
delay: 10.1
options: UIViewAnimationOptionCurveEaseIn
animations:^{
addNameViewConstraint.constant = 10;
addNameView.alpha = 1.0;
[self.view layoutIfNeeded]; // move
}
completion:nil];
[UIView animateWithDuration:0.25
delay: 0.15
options: UIViewAnimationOptionCurveEaseIn
animations:^{
addEmailViewConstraint.constant = 10;
addEmailView.alpha = 1.0;
[self.view layoutIfNeeded]; // move
}
completion:nil];
When I was using the push segue the animation works fine. But when I switched to using a modal segue the animation stopped working. I increased the delay in the first one to 10.2 seconds thinking that maybe it was animating before I got to see it. I am calling this animation in the viewWillAppear method. Again works if I'm doing a push segue.. but not for modal. Any ideas?
I am calling this animation in the viewWillAppear method
This is a common mistake. viewWillAppear: is too soon to start the animation. The view has not yet appeared so there is nothing to animate. Move the animateWithDuration:... code to viewDidAppear: and all will be well.
This is, however, as you say in your comment, insufficiently satisfying. What you are after is that the modal transition should be happening and your extra animations within the new view should already be happening as the modal view is in the process of appearing. Instead, with viewDidAppear:, the modal view finishes appearing, it settles into place, and then your other animations start, which is not as cool.
One solution might be to move the animations again, this time to viewWillLayoutSubviews. This is trickier because this method gets called a lot. You will need to use a BOOL instance variable as a flag to ensure that your animations run only once. Thus, this should work (I tried it and it seemed fine):
-(void)viewWillLayoutSubviews {
if (!self->didLayout) {
self->didLayout = YES;
[UIView animateWithDuration:0.25 animations:^{
// mess with constraint constants here
[self.subv layoutIfNeeded];
}];
}
}

Creating interruptible animation w/ completion block in Objective-C for iOS?

I am trying to create animation behavior wherein a view rotates 45 degrees clockwise and then adds a red circle when the user drags its center above a certain line. Dragging this same view back below the same line would revert it back to its original orientation and then remove the red circle. The view should take 3 seconds to rotate 45 degrees with animation curve UIViewAnimationCurveEaseInOut:
I have two functions that embody this behavior, - (void)viewHasMovedAboveLine:(UIView *)view and - (void)viewHasMovedBelowLine:(UIView *)view, that are called when the view is dragged above or below the line by the user. Both of these functions contain animations with completion handlers, i.e., + (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion. The completion handler adds or removes the red circle as appropriate.
I can get the animation to rotate correctly if the user happens to drag the view back across the line while it is in the middle of an animation by setting options: to UIViewAnimationOptionBeginFromCurrentState and by checking the value of view.layer.animationKeys.count prior to executing the animation and removing all animations if there are any currently executing via [view.layer removeAllAnimations].
However, the completion handler of the prior animation still seems to execute even when using [view.layer removeAllAnimations]. Is there a way to stop both the animation and its completion handler if that animation is currently executing?
I would prefer something more elegant than having to create private properties for each animation, e.g., #property (nonatomic) BOOL animation01IsCurrentlyExecuting and #property (nonatomic) BOOL animation02IsCurrentlyExecuting. The ideal solution would encompass a wide variety of animation scenarios containing both animation code and a completion handler.
ALSO: Is there a way to see how far the animation has progressed when it has been interrupted? I myself am more interested in timing (e.g., the animation was interrupted after 2.1 seconds) so that I can make sure that any further animations are properly timed.
The 'finished' parameter of a UIView animation block is very useful for this case.
[UIView animateWithDuration:0.5 animations:^{
//set your UIView's animatable property
} completion:^(BOOL finished) {
if(finished){
//the animation actually completed
}
else{
//the animation was interrupted and did not fully complete
}
}];
For finding out how long an animation progressed before being interrupted, some methods on NSDate could come in handy.
__block NSDate *beginDate = [NSDate new];
__block NSTimeInterval timeElapsed;
[UIView animateWithDuration:0.5 animations:^{
//your animations
beginDate = [NSDate date];
} completion:^(BOOL finished) {
if(finished){
//the animation actually completed
}
else{
//the animation was interrupted and did not fully complete
timeElapsed = [[NSDate date] timeIntervalSinceDate:beginDate];
NSLog(#"%f", timeElapsed);
}
}];

removeAllAnimations not working on successively called animations

I'm trying to set up an image gallery type view where the image is nearly full screen, and the nav controller, toolbar, buttons (to move between images), and slider (to quickly move between images) all fade out after periods without interaction, and then return on a tap. What I have so far (which I'm sure isn't even close to the right way to do this, I'm something of a beginner) is this:
-(void)fadeOutViews{
[self fadeOutView:rightButton];
[self fadeOutView:leftButton];
[self fadeOutView:mySlider];
mySlider.enabled = NO;
[self fadeOutView:myToolbar];
[self fadeOutView:self.navigationController.navigationBar];
}
-(void)fadeOutView:(UIView *)view{
view.alpha = 1;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:2];
view.alpha = 0;
[UIView commitAnimations];
}
-(void)stopFadeOut{
[rightButton.layer removeAllAnimations];
[leftButton.layer removeAllAnimations];
[mySlider.layer removeAllAnimations];
[myToolbar.layer removeAllAnimations];
[self.navigationController.navigationBar.layer removeAllAnimations];
}
-(void)resetToInitialConfigurationWithDelay:(int)delay{
if (buttonWasPressed){
delay = 4;
buttonWasPressed = NO;
}
rightButton.alpha = 1;
leftButton.alpha = 1;
mySlider.alpha = 1;
myToolbar.alpha = 1;
self.navigationController.navigationBar.alpha = 1;
mySlider.enabled = YES;
[self stopFadeOut];
[self performSelector:#selector(fadeOutViews) withObject:nil afterDelay:delay];
}
So, the theory is, you reset to the initial state (the delay is because images fade in and out when using the buttons to advance, so there needs to be more time in before fading after a button press, or else the fading started immediately after the new image loaded. This resets everything to how it started out, and begins the process of fading again. And stopFadeOut removes all the animations if something occurs that should stop the fading process. So, for example, if a tap occurs:
- (IBAction)tapOccurs:(id)sender {
[self stopFadeOut];
[self resetToInitialConfigurationWithDelay:2];
}
Any previous animations are stopped, and then the process is restarted. Or at least that's the theory. In practice, if, say, there are several taps in quick succession, the faded views will start to fade briefly, and the reset, over and over again, so that it looks like they are flashing, until they finally fade out completely. I thought that perhaps the issue was the the animations were delayed, but the removeAllAnimation calls were not, so I replaced
[self stopFadeOut];
with
[self performSelector:#selector(stopFadeOut) withObject:nil afterDelay:2];
but the results were the same. The behavior is EXACTLY the same if stopFadeOut is never called, so the only conclusion I can draw is that for whatever reason, the removeAllAnimations calls aren't working. Any ideas?
What is happening
It sounds to me like the reset method is called multiple times before the previous run finished. You could easily verify this by adding log-statements to both the tap and reset method and count the number of logs for each method and watch what happens when you tap multiple times in a row.
I've tried to illustrate the problem with drawing below.
T = tapOccurs:
O = fadeOutViews:
--- = wait between T & O
Normal single tap
T-----O T-----O
---------------------> time
Multiple taps in a row
T-----O
T-----O
T-----O
---------------------> time
What it sound like you are trying to do
T-
T---
T-----O
---------------------> time
Every time fadeOutViews: (called O in the illustration) gets called the view will fade out. Looking at your fadeOutView: implementation this means that the opacity will jump to 1 and then fade slowly to 0, thus it looks like they are flashing an equal number of times to the number of taps until finally starting over.
How you can prevent this
You could do a number of things to stop this from happening. One thing would be to cancel all the scheduled reset methods by calling something like cancelPerformSelectorsWithTarget: or cancelPreviousPerformRequestsWithTarget:selector:object:.

Detecting if a Cocoa Animation is in Progress (Using BOOL) to prevent multiple simultaneous animations

In my app, I run numerous animations, moving objects and shapes etc.
Many of the animations lead on to the animation of other objects.
A problem with the application is that if an animation is running and you start another that involves one of the objects currently being animated, the screen goes white/we get unexpected results.
I worked up some sample code to show what we are trying to do:
UIButton *button;
BOOL currentlyShowingQuestion;
- (void)viewDidLoad
{
[self moveButton:button];
currentlyShowingQuestion = NO;
[NSTimer scheduledTimerWithTimeInterval:.1 target:self selector:#selector(hasBoolChanged) userInfo:nil repeats:YES];
}
- (void)moveButton:(UIButton *)buttonToUse {
currentlyShowingQuestion = YES;
NSLog(#"moveButton Called! Yippee!!!!!!!");
[UIView animateWithDuration:0.5 delay:0.5 options:nil animations:^{
buttonToUse.frame = CGRectMake(10, 10, 300, 440);
} completion:^(BOOL finished){
currentlyShowingQuestion = NO;
}];
}
-(void)hasBoolChanged {
if (currentlyShowingQuestion == YES) {
NSLog(#"Yes");
}
else if (currentlyShowingQuestion == NO) {
NSLog(#"No");
}
}
What I end up actually seeing is like the currentlyShowingQuestion BOOL both get changed instantly when the method is run, so it changes to YES and instantly back to NO. Then the animation runs. Why is this? What's wrong with the order?
Any help appreciated.
Sorry about the code formatting - I couldn't figure out the syntax here.
Thanks in advance,
Sam
I don't really understand what you try to achieve. At the beginning you start a timer that checks whether the value of the boolean has changed. I think it is kind of an overkill for this task but thats ok.
Your animation is delayed 0.5s for some reason and then you have a 0.5s duration for an animation and that is really fast so it could seem that you instantly change the value.
I'm not too familiar with UIView, but according to the docs, the completion block may be called before the animation's finished. Try add a condition whether finished is YES before you change currentlyShowingQuestion to NO.

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.