I have a variable with a UIViewPropertyAnimator * which is currently in the process of animating. For example, here's a UIViewPropertyAnimator named fadeOutAnimator, which "fades out" a UILabel (named label), and then removes it from its superview:
UIViewPropertyAnimator *fadeOutAnimator = [[UIViewPropertyAnimator alloc] initWithDuration:1.5f curve:UIViewAnimationCurveLinear animations:^{
[label setAlpha:0.0f];
}];
[fadeOutAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
[label removeFromSuperview];
}];
Let's say that some other event happens in my app that makes me want to have this animation skip to the end, and execute its completion handler.
I've tried using the stopAnimation method with both its YES and NO arguments, but in both cases, the animation display just appears to halt at its current progress; it doesn't advance to the end.
How can I make the animation immediately skip to the end, and execute its completion handler?
This can be done by setting the fractionComplete property of the UIViewPropertyAnimator to 1.0 (i.e. 100% complete). Example:
[fadeOutAnimator setFractionComplete:1.0f];
This does immediately advance the animation to its ending state, and triggers a call to the completion handler(s) (if any).
Related
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
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);
}
}];
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:.
so here is the deal:
I have a label (NSTextField) that I want to activate after I click a button. This label will appear while the program is loading some wavs (since it usually makes a minor delay when it does). I then want it gone once this has happened (and the new View appears).
Now, the problem I have is that this update does not seem to happen when I tried this. If I don't make it disappear at the end then I can see it, but only after the delay has occured (rendering it pointless).
Currently I am using:
[label2 setHidden:NO];
I understand that this will occur once the method I called it in has finished (which is a problem). Any idea what I could do instead so that the label is shown while the program is loading wavs?
Thanks heaps!!
Ok, I guess I solved it myself - I hope this helps people.
So when I click the button I disable the buttons and replace the label temporarily. This, however, only happens in the next view (so I'm not sure how to make it occur in the same view).
I disable the buttons for about 1 second, and it is here that the label is shown.
Here's some code to show what I mean:
- (IBAction)clickedTheButton:(id)sender {
[button setEnabled:NO];
[label2 setHidden:NO];
...
//Changes the View
[self nextMethod];
}
The View has now changed, and this method is called next. This enables me to see the label.
-(void)nextMethod{
...
[self performSelector:#selector(delayedDisplay:)
withObject:#"Hi"
afterDelay:1.0]; //delay for 1 second
}
This method then puts them back to their original state (so the label is hidden and the button is activated again)
-(void) delayedDisplay:(NSString *)string{
[button setEnabled:YES];
[label2 setHidden:YES];
}
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];
}];
}];
}];