UIView progressive animation - ios

The standard UIView animateWithDuration: block works great for animations that have require a single animation effect, i.e. resize and/or move.
Is there a way to make the animation progressive, such that the animation starts slow, and gains speed as it progresses?
I could try nested animateWithDuration: blocks, placing subsequent blocks in the completion handler, but that way the animation is a little 'ragged'. I wish to make the animation smooth.
One idea that comes to mind is that I create a recursive function as follows:
- (void) animateToYOrigin:(CGFloat)yOrigin{
if (myView.frame.origin.y < 1){
[UIView animateWithDuration:0.1
animations:^{
CGRect rect = myView.frame;
rect.origin.y = yOrigin;
myView.frame = rect;
} completion:^(BOOL finished) {
[self animateToYOrigin:yOrigin /2];
}];
}
}
I am looking for a refined solution.

You can use
[UIView animateWithDuration:time
delay:delay
options:OPTION_HERE
animations:anims
completion:completion]
method and pass UIViewAnimationOptions where it says OPTION_HERE. You can use basic ease in/out options by default. If you need more options you can check out this git repo. In MTTimingFuncations.h/c you can find multiple options you can pass.

Related

how to add bounce animation to animateWithDuration?

I have a simple animation that im performing in my scroll view delegate method scrollViewDidEndDragging.
It looks like this:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
NSLog(#"finger was lifted");
[UIView animateWithDuration:1.0
animations:^{
self.homeLabel.frame = self.view.frame;
}];
}
Using this animation after lifting the finger the my homeLabel is coming from top, and i want to add it a bounce animation to the label, so when it comes from top, instead of landing smoothly it will have a nice bounce...how can i DO THAT? thanksss
You can use the usingSpringWithDamping animation function.
[UIView animateWithDuration:1.0 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:5.0 options:UIViewAnimationOptionCurveLinear animations:^{
self.homeLabel.frame = self.view.frame;
} completion:^(BOOL finished) {
}];
Adjusting the Spring Damping and Initial Spring Velocity can give you the effect you want.
One good solution is to create a custom layer for your view that overrides the addAnimation:forKey: method to include a custom timing function.
This answer goes into the specifics of how to do that.
Another option is to take a look at key frame animation. This question and answer covers that approach very well.

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)

iOS: UIView Animation Refactoring

Whilst playing around with UIView animation, I came across a situation where I think some refactoring is needed:
The following views whose opacity are initially set to 0.0f.
Ex:
[UIView animateWithDuration:1.0f
animations:^
{
firstView.layer.opacity = 1.0f;
}
completion:^(BOOL finished)
{
[UIView animateWithDuration:1.0f
animations:^
{
secondView.layer.opacity = 1.0f;
firstView.layer.opacity = 0.0f;
}
completion:^(BOOL finished)
{
[UIView animateWithDuration:1.0f
animations:^
{
thirdView.layer.opacity = 1.0f;
secondView.layer.opacity = 0.0f;
}
completion:^(BOOL finished)
{
thirdView.layer.opacity = 0.0f;
}];
}];
}];
All 3 views are just subclass of UIView's, which are added as subviews of the main view.
This simply animates the opacity of the first view to 1.0f and then that of the second view, and then that of the third view.
Simple. Nothing special here.
My Question is:
What if I had more views, let say 100, that I wanted to perform the same action (same sequence of animation), this block of code would expand and expand.
So for the sake of refactoring and being adhered to good practice of writing code, I thought may be this could be done with less code via the use of a method and perhaps a loop.
Could you enlighten me on this in regards to refactoring; in addition, would dispatch_apply be useful here along with the refactoring process if a loop is needed?
If you wanted to animate 100 images, you would probably want to use 2 views and load alternating images into each one. I recently created a sample app on github that does exactly that:
Animating UIImages with cross-fade opacity changing

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");}];

continuous animation within UITableViewCell

I'm trying to add a continuous animation onto my UITableViewCell-Subclass.
It's a rather easy one with an Image fading in and out (fading between 0.4 alpha and 1.0),
what I've tried so far ist the following:
-(void)animateRecordingIndicator{
[UIView beginAnimations:#"RecordingIndicatorAnimation" context:nil];
[UIView setAnimationDuration:0.3];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationFinished)];
if (animatedImageView.alpha == 0.4)
animatedImageView.alpha = 1.0;
else
animatedImageView.alpha = 0.4;
[UIView commitAnimations];
}
the code within animationFinished is as follows:
-(void)animationFinished{
if (doShowAnimation) {
[self performSelectorOnMainThread:#selector(animateRecordingIndicator) withObject:nil waitUntilDone:YES];
}
}
what I expect should be clear by now, but what I get is simply a crash with Xcode loading Stackframes more or less eternally :)
According to the UIView class reference, you are now discouraged from using the commitAnimations method. Instead use the following:
animateWithDuration:delay:options:animations:completion:
I imagine the infinite recursion you are encountering is related to Apple's reasons for making that recommendation.

Resources