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);
}
}];
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 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];
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:.
I want this to be a background in my app for Ipad.
I'm building everything in objective C (native app)
I need a little help figuring out how to animate each of the triangles over the image (overlay) so it fades in and out independently of each other, the goal to make a constant shimmer like effect so the image doesn't feel sos tatic. Do i have to animate each triangle independently? Is there any algorithm that i should be looking at so it seems kinda random but isn't.
Here is the background image
I need some guidance on where to start and how to approach this problem, and feedback would be appreciated.
a) My advice is to use UIViewAnimationWithBlocks introduced in iOS 4. If you have a solid grasp on blocks, they can be very useful. Here's an example I created in as little as 5 minutes to illustrate:
typedef void(^FadeInOutBlock)(void);
#interface PMViewController ()
#property (nonatomic, copy) FadeInOutBlock fadeInOutBlock;
#end
Here we declare a typedef to save us from doing the block syntax all over again. We also create a property to hold the animation block.
#implementation PMViewController
#synthesize myView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
__block PMViewController *_self = self;
self.fadeInOutBlock = ^{
[UIView animateWithDuration:0.5f animations:^{
// fade out effect
_self.myView.alpha = 0.0f;
} completion:^(BOOL success){
[UIView animateWithDuration:0.5f animations:^{
// fade in effect
_self.myView.alpha = 1.0f;
} completion:^(BOOL success){
// recursively fire a new animation
if (_self.fadeInOutBlock)
_self.fadeInOutBlock();
}];
}];
};
}
We create the animation, within an animation. You start off with the fade out, where myView's alpha will be reduced to 0.0f in 0.5f seconds. After it's completed, a second animation will be fired, restoring the alpha for myView back to 1.0f and finally, firing out the first animation, again. (Animception)
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (self.fadeInOutBlock)
self.fadeInOutBlock();
}
#end
And finally, in view did appear you fire it off for the first time.
b)
Now, for the shimmering animation that you mention, I suggest you separate each triangle into it's own UIView and use the technique above, using different durations and alphas.
If you have to many small UIViews, group them up into a bigger UIView (by using the addSubview method) and apply the animation to those 'container' UIViews.
For instance, you could have four separate UIView containers, that have a bunch of separate UIViews as their children. You could then create four block animations, one for each container, and then apply the animation to them. I bet experimenting with that, you would be able to create pretty good effects.
You can do this without any special librarys. So you need to create a black UIView with an alpha of 0. Then create an NSTimer that increases the alpha of the UIView.
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.