Force UIView animateWithDuration completion block to be called - ios

I have two animations which are overlapping, which, because of the way I've set my method up, causes the second one not to fire. I have a check like so in the beginning of the method:
- (void)animateHidden:(BOOL)hidden duration:(CGFloat)seconds delay:(CGFloat)delay options:(UIViewAnimationOptions)options disableUserInteraction:(BOOL)disableUserInteraction {
if (self.hidden == hidden) {
return;
}
Then, further down, my animation block looks like so:
__weak UIView *weakSelf = self;
[UIView animateWithDuration:seconds delay:delay options:options animations:^{
weakSelf.alpha = hidden ? 0 : 1;
} completion:^(BOOL finished) {
// Return user interaction to previous state
if (disableUserInteraction) {
weakSelf.userInteractionEnabled = userInteractionEnabled;
}
weakSelf.hidden = hidden;
}];
Two animations are kicked off on the same view, one before a service call and one after. If the service call happens quick enough that the view is still animating, weakSelf.hidden = hidden; will never be called, and the second animation will exit out since the hidden value wasn't updated in time.
Is there anyway that I could force the completion block on the animation block to be called? I need to update my hidden property before making the check, but can't find a way to accomplish this.
Calling [self.layer removeAllAnimations] doesn't seem to work unfortunately.

You can use CABasicAnimation instead of UIView animation, that will solve the problem more accurately.
You can use it like:
CABasicAnimation* opacityZero= [CABasicAnimation animationWithKeyPath:#"opacity"];
[opacityZero setToValue:[NSNumber numberWithFloat:0.0]];
[opacityZero setDuration:duration];
[[self layer] addAnimation:opacityZero forKey:#"opacityZero"];
And when your service call ends, you can call [self.layer removeAllAnimations];
Similarly, you can make the opacity one and tweak the above method as you like.
You can find more info here.

A __block prefixed to hidden attribute declaration should help.
Something like, #property (nonatomic) __block BOOL hidden;

If you are targeting iOS 10+ take a look at UIViewPropertyAnimator
https://developer.apple.com/reference/uikit/uiviewpropertyanimator?language=objc
Combined with the UIViewAnimating and UIViewImplicitlyAnimating protocols, this allows modification / interruption / pause / resume / stop / etc of the animations.
Basic example (buttons and view set in IB):
- (IBAction)startTapped:(id)sender {
_myAnimator = [UIViewPropertyAnimator
runningPropertyAnimatorWithDuration:3.0
delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^{
_theRedBox.alpha = _theRedBox.alpha > 0 ? 0 : 1;
} completion:^(UIViewAnimatingPosition finalPosition) {
// do stuff
}];
}
- (IBAction)stopTapped:(id)sender {
[_myAnimator stopAnimation:NO];
[_myAnimator finishAnimationAtPosition:UIViewAnimatingPositionEnd];
}

Related

Stop specific UIViewAnimation block - iOS

I have an iOS application which runs a few different UIViewAnimation blocks to animate a few different objects on screen. This all works, but how can I stop ONE of the animation blocks WITHOUT stopping the rest?
I have tried using the following method:
[button.layer removeAllAnimations];
But it doesn't do anything, the animation just continues.
I then tried to use a simple BOOL value and get the animation to return from the method once the BOOL is set to "NO", but that doesn't work either.
Here is the animation which I am trying to stop:
-(void)undo_animation:(int)num {
// Fade out/in the number button label
// animation which lets the user know
// they can undo this particular action.
[UIView animateWithDuration:0.5 delay:0.2 options:(UIViewAnimationOptionRepeat | UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionAllowUserInteraction) animations:^{
// Mostly fade out the number button label.
((UIButton *)_buttons[num]).alpha = 0.2;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 delay:0.2 options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionAllowUserInteraction) animations:^{
// Fade in the number button label.
((UIButton *)_buttons[num]).alpha = 1.0;
} completion:^(BOOL finished) {
// Stop the animation if the BOOL
// is set to 'NO' animations.
if (anim_state_button == NO) {
return;
}
}];
}];
}
Thanks, Dan.
You can't reach running animation property if you're using UIKit Animations. So I suggest to using Core Animation if you want to modify animation in the runtime of it.
And it's too simple to removing alpha of the view like below.
CABasicAnimation* fadein= [CABasicAnimation animationWithKeyPath:#"opacity"];
[fadein setToValue:[NSNumber numberWithFloat:1.0]];
[fadein setDuration:0.5];
[[moviepic layer]addAnimation:fadein forKey:#"MyAnimation"];
after adding animation to layer animation will start and than you can use delegate methods to be informed about animationDidFinish: method
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
NSLog(#"Animation interrupted: %#", (!flag)?#"Yes" : #"No");
}
Also you can reach whenever you want from using;
[[moviepic layer] animationForKey:#"MyAnimation"];
And of course you need to add CoreAnimation Framework to your project.
Hope it helps.
I think the simple way is that remove all animations from your View Layer as all the animation are by default added into the View's Layer.
[yourRequiredView.layer removeAllAnimations];
My understanding is that removing all animations from the relevant layer should stop all animations. How about [((UIButton *)_buttons[num]).layer removeAllAnimations]; ?

How to properly animate UIScrollView contentOffset

I have UIScrollView subclass. Its content is reusable - about 4 or 5 views are used to display hundreds of elements (while scrolling hidden objects reused and jumps to another position when its needed to see them)
What i need: ability to automatically scroll my scroll view to any position. For example my scroll view displays 4th, 5th and 6th element and when I tap some button it needs to scroll to 30th element. In other words I need standard behaviour of UIScrollView.
This works fine:
[self setContentOffset:CGPointMake(index*elementWidth, 0) animated:YES];
but I need some customisation. For example, change animation duration, add some code to perform on end of animation.
Obvious decision:
[UIView animateWithDuration:3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[self setContentOffset:CGPointMake(index*elementWidth, 0)];
} completion:^(BOOL finished) {
//some code
}];
but I have some actions connected to scroll event, and so now all of them are in animation block and it causes all subview's frames to animate too (thanks to few reusable elements all of them animates not how i want)
The question is: How can I make custom animation (in fact I need custom duration, actions on end and BeginFromCurrentState option) for content offset WITHOUT animating all the code, connected to scrollViewDidScroll event?
UPD:
Thanks to Andrew's answer(first part) I solved issue with animation inside scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[UIView performWithoutAnimation:^{
[self refreshTiles];
}];
}
But scrollViewDidScroll must (for my purposes) executes every frame of animation like it was in case of
[self setContentOffset:CGPointMake(index*elementWidth, 0) animated:YES];
However, now it executes only once at start of animation.
How can I solve this?
Did you try the same approach, but with disabled animation in scrollViewDidScroll ?
On iOS 7, you could try wrapping your code in scrollViewDidScroll in
[UIView performWithoutAnimation:^{
//Your code here
}];
on previous iOS versions, you could try:
[CATransaction begin];
[CATransaction setDisableActions:YES];
//Your code here
[CATransaction commit];
Update:
Unfortunately that's where you hit the tough part of the whole thing. setContentOffset: calls the delegate just once, it's equivalent to setContentOffset:animated:NO, which again calls it just once.
setContentOffset:animated:YES calls the delegate as the animation changes the bounds of the scrollview and you want that, but you don't want the provided animation, so the only way around this that I can come up with is to gradually change the contentOffset of the scrollview, so that the animation system doesn't just jump to the final value, as is the case at the moment.
To do that you can look at keyframe animations, like so for iOS 7:
[UIView animateKeyframesWithDuration:duration delay:delay options:options animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
[self setContentOffset:CGPointMake(floorf(index/2) * elementWidth, 0)];
}];
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
[self setContentOffset:CGPointMake(index*elementWidth, 0)];
}];
} completion:^(BOOL finished) {
//Completion Block
}];
This will get you two updates and of course you could use some math and a loop to add up a lot more of these with the appropriate timings.
On previous iOS versions, you'll have to drop to CoreAnimation for keyframe animations, but it's basically the same thing with a bit different syntax.
Method 2:
You can try polling the presentationLayer of the scrollview for any changes with a timer that you start at the beginning of the animation, since unfortunately the presentationLayer's properties aren't KVO observable. Or you can use needsDisplayForKey in a subclass of the layer to get notified when the bounds change, but that'll require some work to set up and it does cause redrawing, which might affect performance.
Method 3:
Would be to dissect exactly what happens to the scrollView when animated is YES try and intercept the animation that gets set on the scrollview and change its parameters, but since this would be the most hacky, breakable due to Apple's changes and trickiest method, I won't go into it.
A nice way to do this is with the AnimationEngine library. It's a very small library: six files, with three more if you want damped spring behavior.
Behind the scenes it uses a CADisplayLink to run your animation block once every frame. You get a clean block-based syntax that's easy to use, and a bunch of interpolation and easing functions that save you time.
To animate contentOffset:
startOffset = scrollView.contentOffset;
endOffset = ..
// Constant speed looks good...
const CGFloat kTimelineAnimationSpeed = 300;
CGFloat timelineAnimationDuration = fabs(deltaToDesiredX) / kTimelineAnimationSpeed;
[INTUAnimationEngine animateWithDuration:timelineAnimationDuration
delay:0
easing:INTULinear
animations:^(CGFloat progress) {
self.videoTimelineView.contentOffset =
INTUInterpolateCGPoint(startOffset, endOffset, progress);
}
completion:^(BOOL finished) {
autoscrollEnabled = YES;
}];
Try this:
UIView.animate(withDuration: 0.6, animations: {
self.view.collectionView.contentOffset = newOffset
self.view.layoutIfNeeded()
}, completion: nil)

2 animations on same object, only last shows. How to show first one w/o nesting completion block?

I'm doing two animations on the same UIImageView, using blocks. Animations are not quite back to back, but almost; there is some logic inbetween.
animate UIImageView from one location on the view to another.
execute logic that determines whether image is allowed to stay there
if not allowed, undo the animation (UIImageView springs back to original location)
If I implement this as above, only the second animation shows (this is normal behavior from what I understand). If I nest the logic and the second animation block inside the completion block of the first, I see both animations, but there's a fair amount of code to jam into that completion block and it just seems ugly and out of place.
In the non-nested configuration, why does iOS want to cut short the previous animations and execute only the final one, and how can I force it to wait on the first one before going to the next? I don't think it needs to block the main thread or "sit and spin" in a completion block; I just want all animations to be shown. Tried adding delay to second animation to no avail.
Is this a job for CAKeyframeAnimation?
// first animation
[UIView animateWithDuration:0.5
delay:0.0
options: UIViewAnimationCurveLinear
animations:^{movingColor.center = newCenter;
movingColor.transform = scaleTransform;}
completion:^(BOOL finished){ NSLog(#"forward animation done");}];
if (/* ...color is allowed at that location */) {
// do some stuff
} else {
// undo the animation
[UIView animateWithDuration:0.5
delay:0.0
options: UIViewAnimationCurveLinear
animations:^{movingColor.center = origCenter;
movingColor.transform = scaleTransform;}
completion:^(BOOL finished){ NSLog(#"back animation done");}];
}
The second animation should be done conditionally inside the first's completion block. Putting it into a function will make it more readable.
- (void)moveColor:(UIView *)view to:(CGPoint)center delay:(NSTimeInterval)delay completion:(void (^)(BOOL finished))complete {
[UIView animateWithDuration:0.5 delay:delay options:UIViewAnimationCurveLinear animations:^{
view.center = center;
view.transform = scaleTransform; // probably don't need this
} completion:completion];
}
This way your logic can be separate from the animation code
- (void)someMethod {
[self moveColor:movingColor to:newCenter delay:0.0 completion:^(BOOL finished) {
if (/*it can stay*/) {
// stuff
} else {
[self moveColor:movingColor to:origCenter delay:2.0 completion:^(BOOL finished) {}];
}
}];
}
The correct way to do it is as you said to set the second one in the completition of the first one that is the correct way,
You can also adda delay on the start of the other, this may or may not work it depends on alot of variables
Your second animation will have a delay of .5 (time for first animation to complete )
[UIView animateWithDuration:0.5
delay:0.5
options: UIViewAnimationCurveLinear
animations:^{movingColor.center = origCenter;
movingColor.transform = scaleTransform;}
completion:^(BOOL finished){ NSLog(#"back animation done");}];
The animateWithDuration is executed asynchronously, and returns right away. That is, the next line of code is executed without waiting for the animation to finish. You either put your code in the completion block if you want it to be executed after the animation is finished, or you accept that the second animation is started (thus canceling the first) immediately.
If you want to indicate to the user that something was wrong by starting the animation, but not completing it, you could execute the second animation with a delay. Remember to also set the the UIViewAnimationOptionBeginFromCurrentState option if you delay the second animation with less than the duration of the first:
[UIView animateWithDuration:0.5
delay:0.25 //or some number
options: (UIViewAnimationCurveLinear | UIViewAnimationOptionBeginFromCurrentState)
animations:^{movingColor.center = origCenter;
movingColor.transform = scaleTransform;}
completion:^(BOOL finished){ NSLog(#"back animation done");}];

CAGradientLayer properties not animating within UIView animation block

I have a feeling I'm overlooking something elementary, but what better way to find it than to be wrong on the internet?
I have a fairly basic UI. The view for my UIViewController is a subclass whose +layerClass is CAGradientLayer. Depending on the user's actions, I need to move some UI elements around, and change the values of the background's gradient. The code looks something like this:
[UIView animateWithDuration:0.3 animations:^{
self.subview1.frame = CGRectMake(...);
self.subview2.frame = CGRectMake(...);
self.subview2.alpha = 0;
NSArray* newColors = [NSArray arrayWithObjects:
(id)firstColor.CGColor,
(id)secondColor.CGColor,
nil];
[(CAGradientLayer *)self.layer setColors:newColors];
}];
The issue is that the changes I make in this block to the subviews animate just fine (stuff moves and fades), but the change to the gradient's colors does not. It just swaps.
Now, the documentation does say that Core Animation code within an animation block won't inherit the block's properties (duration, easing, etc.). But is it the case that that doesn't define an animation transaction at all? (The implication of the docs seems to be that you'll get a default animation, where I get none.)
Do I have to use explicit CAAnimation to make this work? (And if so, why?)
There seem to be two things going on here. The first (as Travis correctly points out, and the documentation states) is that UIKit animations don't seem to hold any sway over the implicit animation applied to CALayer property changes. I think this is weird (UIKit must be using Core Animation), but it is what it is.
Here's a (possibly very dumb?) workaround for that problem:
NSTimeInterval duration = 2.0; // slow things down for ease of debugging
[UIView animateWithDuration:duration animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
// ... do stuff to things here ...
[CATransaction commit];
}];
The other key is that this gradient layer is my view's layer. That means that my view is the layer's delegate (where, if the gradient layer was just a sublayer, it wouldn't have a delegate). And the UIView implementation of -actionForLayer:forKey: returns NSNull for the "colors" event. (Probably every event that isn't on a specific list of UIView animations.)
Adding the following code to my view will cause the color change to be animated as expected:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
if( [#"colors" isEqualToString:event]
&& (nil == action || (id)[NSNull null] == action) ) {
action = [CABasicAnimation animationWithKeyPath:event];
}
return action;
}
You have to use explicit CAAnimations, because you're changing the value of a CALayer.
UIViewAnimations work on UIView properties, but not directly on their CALayer's properties...
Actually, you should use a CABasicAnimation so that you can access its fromValue and toValue properties.
The following code should work for you:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[UIView animateWithDuration:2.0f
delay:0.0f
options:UIViewAnimationCurveEaseInOut
animations:^{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"colors"];
animation.duration = 2.0f;
animation.delegate = self;
animation.fromValue = ((CAGradientLayer *)self.layer).colors;
animation.toValue = [NSArray arrayWithObjects:(id)[UIColor blackColor].CGColor,(id)[UIColor whiteColor].CGColor,nil];
[self.layer addAnimation:animation forKey:#"animateColors"];
}
completion:nil];
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
NSString *keyPath = ((CAPropertyAnimation *)anim).keyPath;
if ([keyPath isEqualToString:#"colors"]) {
((CAGradientLayer *)self.layer).colors = ((CABasicAnimation *)anim).toValue;
}
}
There is a trick with CAAnimations in that you HAVE to explicitly set the value of the property AFTER you complete the animation.
You do this by setting the delegate, in this case I set it to the object which calls the animation, and then override its animationDidStop:finished: method to include the setting of the CAGradientLayer's colors to their final value.
You'll also have to do a bit of casting in the animationDidStop: method, to access the properties of the animation.

IOS Core animation, how do I call animationDidStop: method when my animation sequence ends?

I have the following sequence of images to be animated:
IBOutlet UIImageView *block;
NSArray *image_arr = [NSArray arrayWithObjects:[UIImage imageNamed:#"img1"],[UIImage imagedNamed:#"img2"],nil];
block.animationImages = image_arr;
block.animationDuration =1;
block.animationRepeatcount =5;
[block startAnimating];
And the method animationDidStop:
-(void)animationDidStop:(NSString*)animationID finished:(BOOL)didFinish context:(void*)context{
NSLog(animationID);
}
What do I need to do to have the method animationDidStop called at the end of the animation?
UIImageView's "animationImages" property is not related to coreanimation, it just triggers an internal NSTimer that switched the image.
The
-(void)animationDidStop:finished:context:
selector is called as a part of core animation.
What I'd try in your case is something like this:
IBOutlet UIImageView *block;
NSArray *image_arr = [NSArray arrayWithObjects:[UIImage imageNamed:#"img1"],[UIImage imagedNamed:#"img2"],nil];
block.animationImages = image_arr;
block.animationDuration = animationDuration;
block.animationRepeatcount = animationRepeatCount;
[block startAnimating];
[self performSelector:#selector(animationFinished:) withObject:#"someAnimationId" afterDelay: animationDuration * animationRepeatCount];
Keep in mind that this method can cause some lag - if the animation frames' images are displayed for the first time as a part of the animation, lazy loading kicks in before the first time the animation starts playing, and it may effect the precision of the performSelector call.
If this turns out to be a problem check out this answer's forceLoad method, and if that doesn't work call startAnimation on a hidden UIImageView, it did the trick for me once.
What do I need to do to have the method animationDidStop called at the end of the animation?
You can't have it called, because this is not Core Animation. You have mentally conflated two different things:
The ability to do very simple-minded cartoon-style animation by having a UIImageView show a succession of images;
Core Animation, in which there can be a completion block or completion handler.
What I suggest you do is either use real Core Animation where you can actually set up an animation and a completion block or handler, or else just fake it by using delayed performance to call back into your own code 5 seconds from now when this pseudo-animation is over.
With the latest iOS Versions 4.2 and later you should be using Blocks for animations
The following will push you in the right direction, try changing the UIViewAnimationOption to something different:
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionRepeat animations:^{
//Animation here
}completion:^(BOOL finished) {
if(finished){
//Your Code here
}
}

Resources