UIView animations in a loop - objective-c

Is there a reason why animations do not work in a loop? I know you can do the command:
[UIView setAnimationRepeatCount: someNumber];
which was working fine until I put actual different pdfs to be loaded. So before, with just one pdf that would always get loaded, when I did the animation say 5 times to go to the last document, the animations would look correct. But now that I have 5 different pdfs, it will flip the page 5 times with the CurlUp but it will keep showing the 1st pdf in the list, and then at the end load the proper one. So I thought I could do it in a loop, keeping track of the location in the array, and load each pdf at each location as i flip through the pages so it gives the user the feeling of where they are in the stack of pdfs. But when I put the core animation code in a loop, it basically shows the animation once, but does get to the correct document.

From my understanding of what you're doing, it sounds like you're actually running all 5 animations at once. When you perform an animation block in a for-loop, the runloop doesn't iterate between each loop and only the last animation gets run.
What you'll need to use is use completion calls and "chain" them together. So you run the first animation, then on that completion call you run the second, then on that call the third, etc. Much more lengthy to code, but this will work.
Would be easier to use blocks in this scenario, then all your animation code will be in one place instead of having to spread it all across a bunch of methods for each animation. You would just nest the blocks, in the first animation's completion block you'd spawn the second, then the third, etc.
Here's an example using the blocks method. It's a little hard to read, but you'll get the idea. You might be able to create a single animation block and reuse it in each completion method, but I don't know all of the catches of doing that, and this is much easier to read when you're first trying to understand it.
Note that I haven't actually tried this or tested it, but I think it will do what you want.
[UIView animateWithDuration:1.0 animations:^(void) {
// Animation changes go here
// blah.alpha = 1.0
// blah.position = CGPointMake
// etc
} completion:^(BOOL finished) {
// Start next animation which will run with this one finishes
[UIView animateWithDuration:1.0 animations:^(void) {
// Animation changes go here
// blah.alpha = 1.0
// blah.position = CGPointMake
// etc
} completion:^(BOOL finished) {
// Start next animation which will run with this one finishes
[UIView animateWithDuration:1.0 animations:^(void) {
// Animation changes go here
// blah.alpha = 1.0
// blah.position = CGPointMake
// etc
} completion:^(BOOL finished) {
// Start next animation which will run with this one finishes
[UIView animateWithDuration:1.0 animations:^(void) {
// Animation changes go here
// blah.alpha = 1.0
// blah.position = CGPointMake
// etc
} completion:nil];
}];
}];
}];

Related

Repeating NSTimer causes truncated animateWithDuration on first animation

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

UIView animation stop when UIView willDisappear

I am not much into iOS animation. What I am trying to achieve is a simple message view that slide vertical from bottom of screen to a given y, then after few instants the UIView rollback in vertical to go off screen.
[UIView animateWithDuration:0.5f
animations:^{
self.messageView.frame=CGRectMake(x, y -80, width, height);
}
completion:^(BOOL finished){
if (finished) {
[UIView animateWithDuration:0.5f delay:2.0f options:UIViewAnimationOptionCurveLinear
animations:^{
self.messageView.frame=CGRectMake(x, y + 80, width, height);
}
completion:^(BOOL finished){
// do something...
}
];
}
} ];
This is working fine, but I am having a problem using this mechanism in a iOS UITabBar application: when I change tab, the animation stop, I can infact see that "finished" completion is "false". Therefore the second block is not called, and the message view stays on.
Here are the questions:
my first concern is to understand if the code I have written is correct, regarding the nested animations.
I could solve by ignoring 'finished' and execute code anyway, but I don't feel it is a good idea
within the last completion block, I have put some programming logic, basically I am restoring few UIButtons state, and some other little UI change. At this point I don't know if it is a good idea, it seems not, but how can let the UI knows that the message view has disappeared. NSNotification and KVO seems a bad idea when fast responsive UI change are involved.
You can stop all animations for a layer by sending the layer a removeAllAnimations message.
[sel.view removeAllAnimations];

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:.

UIView animation to "rewind" a pan gesture: recursion?

I have a scenario where the user dragged a UIView somewhere; I want to return it to where it started, and I want it to return along the same track and at the same speed that the user dragged. I have come up with a recursive solution that looks like this:
#property (...) NSMutableArray *dragTrack; // recorded during pan gesture
- (void)goHome
{
[self backtrack: [self.dragTrack count]
withDelay: 1.0 / [self.dragTrack count]];
// interim version of delay; will implement accurate timing later
}
- (void)backtrack: (int)idx withDelay: (double)delay
{
if (idx > 0){
[UIView animateWithDuration:0 delay:delay options: 0
animations: ^{
self.center = [[self.dragTrack objectAtIndex:idx - 1] CGPointValue];
}
completion:^(BOOL finished){
[self backtrack: idx - 1 withDelay: delay];
}];
} else {
// do cleanup stuff
}
}
This works, and recursion depth doesn't appear to be an issue - my drag tracks are typically only a couple of hundred points long. (I assume the chances of the recursive call to backtrack getting tail call optimized are rather slim?). But I'm still wondering: is this a reasonable/normal/safe solution to what I'm trying to achieve? Or is there a simpler way to pass a collection of timestamps and states to an animation and say "play these"?
You are not causing recursion there. Since the completion block is called asynchronously, it is not getting deeper into function stack. You can see it yourself by setting a breakpoint there and check the stack trace.
You can imagine NSTimer there triggering theoretically infinite times and not causing recursion.
To your second question, you can make such animation. It is called CAKeyframeAnimation and it is part of QuartzCore framework. It would require you to work with layers instead of views, but it is not that difficult.
Just one note to your code: try to use option UIViewAnimationOptionCurveLinear for the animation to be more smooth.

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.