How to change UIScrollView contentOffset animation speed? - ios

Is there a way to change contentOffset animation speed without creating your own animation that sets the contentOffset?
The reason why I can't create my own animation for contentOffset change is that this will not call -scrollViewDidScroll: in regular intervals during animation.

Unfortunately there is no clean and easy way to do that.
Here is a slightly brutal, but working approach:
1) Add CADisplayLink as a property:
#property (nonatomic, strong) CADisplayLink *displayLink;
2) Animate content offset:
CGFloat duration = 2.0;
// Create CADisplay link and add it to the run loop
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(_displayLinkTick)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[UIView animateWithDuration:duration animations:^{
self.scrollView.contentOffset = newContentOffset;
} completion:^(BOOL finished) {
// Cleanup the display link
[self.displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
self.displayLink = nil;
}];
3) Finally observe the changes on presentationLayer like:
- (void)_displayLinkTick {
CALayer *presentationLayer = (CALayer *)self.scrollView.layer.presentationLayer;
CGPoint contentOffset = presentationLayer.bounds.origin;
[self _handleContentOffsetChangeWithOffset:contentOffset];
}
- (void)_handleContentOffsetChangeWithOffset:(CGPoint)offset {
// handle offset change
}

To get periodic information about the scroll state, you could run the animation in steps. The delegate will get called once (scrollViewDidScroll:) for each step
- (void)scrollTo:(CGPoint)offset completion:(void (^)(BOOL))completion {
// this presumes an outlet called scrollView. You could generalize by passing
// the scroll view, or even more generally, place this in a UIScrollView category
CGPoint contentOffset = self.scrollView.contentOffset;
// scrollViewDidScroll delegate will get called 'steps' times
NSInteger steps = 10;
CGPoint offsetStep = CGPointMake((offset.x-contentOffset.x)/steps, (offset.y-contentOffset.y)/steps);
NSMutableArray *offsets = [NSMutableArray array];
for (int i=0; i<steps; i++) {
CGFloat stepX = offsetStep.x * (i+1);
CGFloat stepY = offsetStep.y * (i+1);
NSValue *nextStep = [NSValue valueWithCGPoint:CGPointMake(contentOffset.x+stepX, contentOffset.y+stepY)];
[offsets addObject:nextStep];
}
[self scrollBySteps:offsets completion:completion];
}
// run several scroll animations back-to-back
- (void)scrollBySteps:(NSMutableArray *)offsets completion:(void (^)(BOOL))completion {
if (!offsets.count) return completion(YES);
CGPoint offset = [[offsets objectAtIndex:0] CGPointValue];
[offsets removeObjectAtIndex:0];
// total animation time == steps * duration. naturally, you can fool with both
// constants. to keep the rate constant, set duration == steps * k, where k
// is some constant time per step
[UIView animateWithDuration:0.1 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
self.scrollView.contentOffset = offset;
} completion:^(BOOL finished) {
[self scrollBySteps:offsets completion:completion];
}];
}
Call it like this...
CGPoint bottomOffset = CGPointMake(0, self.scrollView.contentSize.height - self.scrollView.bounds.size.height);
[self scrollTo:bottomOffset completion:^(BOOL finished) {}];
// BONUS completion handler! you can omit if you don't need it

Below code solved my issue
CGPoint leftOffset = CGPointMake(0, 0);
[UIView animateWithDuration:.5
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
[self.topBarScrollView setContentOffset:leftOffset animated:NO];
} completion:nil];

Here's a simple solution that works.
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[self setContentOffset:<offset> animated:YES];
[UIView commitAnimations];
Hope this helps.
Brian

Related

How to chain CGAffineTransform together in separate animations?

I need two animations on a UIView:
Make the view move down and slightly grow.
Make the view grow even bigger about its new center.
When I attempt to do that, the second animation starts in a weird location but ends up in the right location and size. How would I make the second animation start at the same position that the first animation ended in?
#import "ViewController.h"
static const CGFloat kStartX = 100.0;
static const CGFloat kStartY = 20.0;
static const CGFloat kStartSize = 30.0;
static const CGFloat kEndCenterY = 200.0;
#interface ViewController ()
#property (nonatomic, strong) UIView *box;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.box = [[UIView alloc] initWithFrame:CGRectMake(kStartX, kStartY, kStartSize, kStartSize)];
self.box.backgroundColor = [UIColor brownColor];
[self.view addSubview:self.box];
[UIView animateWithDuration:2.0
delay:1.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:0
animations:^{
self.box.transform = [self _transformForSize:50.0 centerY:kEndCenterY];
}
completion:^(BOOL finished) {
[UIView animateWithDuration:2.0
delay:1.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:0
animations:^{
self.box.transform = [self _transformForSize:100.0 centerY:kEndCenterY];
}
completion:^(BOOL finished) {
}];
}];
}
- (CGAffineTransform)_transformForSize:(CGFloat)newSize centerY:(CGFloat)newCenterY
{
CGFloat newScale = newSize / kStartSize;
CGFloat startCenterY = kStartY + kStartSize / 2.0;
CGFloat deltaY = newCenterY - startCenterY;
CGAffineTransform translation = CGAffineTransformMakeTranslation(0.0, deltaY);
CGAffineTransform scaling = CGAffineTransformMakeScale(newScale, newScale);
return CGAffineTransformConcat(scaling, translation);
}
#end
There's one caveat: I'm forced to use setTransform rather than setFrame. I'm not using a brown box in my real code. My real code is using a complex UIView subclass that doesn't scale smoothly when I use setFrame.
This looks like it might be a UIKit bug with how UIViews resolve their layout when you apply a transform on top of an existing one. I was able to at least get the starting coordinates for the second animation correct by doing the following, at the very beginning of the second completion block:
[UIView animateWithDuration:2.0
delay:1.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:0
animations:^{
self.box.transform = [self _transformForSize:50.0 centerY:kEndCenterY];
}
completion:^(BOOL finished) {
// <new code here>
CGRect newFrame = self.box.frame;
self.box.transform = CGAffineTransformIdentity;
self.box.frame = newFrame;
// </new code>
[UIView animateWithDuration:2.0
delay:1.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:0
animations:^{
self.box.transform = [self _transformForSize:100.0 centerY:kEndCenterY];
}
completion:^(BOOL finished) {
}];
}];
Using the same call to -_transformForSize:centerY: results in the same Y translation being performed in the second animation, though, so the box ends up further down in the view than you want when all is said and done.
To fix this, you need to calculate deltaY based on the box's starting Y coordinate at the end of the first animation rather than the original, constant Y coordinate:
- (CGAffineTransform)_transformForSize:(CGFloat)newSize centerY:(CGFloat)newCenterY
{
CGFloat newScale = newSize / kStartSize;
// Replace this line:
CGFloat startCenterY = kStartY + kStartSize / 2.0;
// With this one:
CGFloat startCenterY = self.box.frame.origin.y + self.box.frame.size.height / 2.0;
// </replace>
CGFloat deltaY = newCenterY - startCenterY;
CGAffineTransform translation = CGAffineTransformMakeTranslation(0.0, deltaY);
CGAffineTransform scaling = CGAffineTransformMakeScale(newScale, newScale);
return CGAffineTransformConcat(scaling, translation);
}
and it should do the trick.
UPDATE
I should say, I consider this "trick" more of a work-around than an actual solution, but the box's frame and transform look correct at each stage of the animation pipeline, so if there's a true "solution" it's eluding me at the moment. This trick at least solves the translation problem for you, so you can experiment with your more complex view hierarchy and see how far you get.

CGAffineTransformIdentity different with iOS8 and XCode6 (Without Autolayout)

I have an animation to add a subview to make it appear as if it comes from inside where the user touches and then fills the entire screen.
Sameway, when an iOS app opens up from the place the user touches it..
- (void) showView : (UIView *) theview : (CGPoint) thepoint {
CGPoint c = thepoint;
CGFloat tx = c.x - floorf(theview.center.x) + 10;
CGFloat ty = c.y - floorf(theview.center.y) + 100;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
// Transforms
CGAffineTransform t = CGAffineTransformMakeTranslation(tx, ty);
t = CGAffineTransformScale(t, 0.1, 0.1);
theview.transform = t;
theview.layer.masksToBounds = YES;
[theview setTransform:CGAffineTransformIdentity];
}
completion:^(BOOL finished) {
}];
}
The above code did, what we needed till iOS8.. Built under XCode5.1 (iOS7 SDK)
But the behaviour was entirely different starting with the iOS8 SDK, XCode6
The ZOOM now is behaving strangely. I was finally able to find that CGAffineTransformIdentity is behaving wrongly(or I use it wrongly?) in iOS8..
I see many have this issue, but they mentioned about AutoLayout. All my views are created programatically. We don't use a nib file.(IB)
How can I make this work with XCode 6?
After couple of hours effort, I came up with a solution. I just post it for future users.
- (void) showView : (UIView *) theview : (CGPoint) thepoint {
CGPoint c = thepoint;
CGFloat tx = c.x - (floorf(theview.center.x)) ;
CGFloat ty = c.y - (floorf(theview.center.y));
/* The transformation now is before the animation block */
CGAffineTransform t = CGAffineTransformMakeTranslation(tx, ty);
t = CGAffineTransformScale(t, 0.1, 0.1);
theview.transform = t;
theview.layer.masksToBounds = YES;
/* Animate only the CGAffineTransformIdentity */
[UIView animateWithDuration:0.8
delay:0.0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
theview.layer.masksToBounds = YES;
[theview setTransform:CGAffineTransformIdentity];
}
completion:^(BOOL finished) {
}];
}
But I have no clue, why it worked with iOS7 SDK previously and not with the new iOS8 SDK.!

Setting UIView with animation to default state

I have an animating sine wave in one of my view controllers that appears every time an up swipe gesture is detected and disappears when a down swipe is detected. The sine wave is a UIView and the animation translates the sine wave across the screen. This works fine when I swipe up and down the first time. However, after the first swipe I get all kinds of issues. I noticed that if I reallocate and initialize the sine wave after every swipe down, I can get it to work properly, but I know thats not the correct fix. My assumption is that the frame of the sine wave has to be reset to what it was originally, but my attempt to do so didn't resolve the issue (my attempt is commented out in the code below). Any ideas?
View controller code:
- (void)viewDidLoad
{
[self setUpSine];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
-(void) setUpSine
{
self.sineWave = [[OSDrawWave alloc] initWithFrame:CGRectMake(-1*self.view.bounds.size.width, self.view.bounds.size.height, 2*self.view.bounds.size.width, .3125*(2*self.view.bounds.size.width))];
[self.sineWave setBackgroundColor:[UIColor clearColor]];
[self.view insertSubview:self.sineWave belowSubview:self.panedView];
}
- (IBAction)handleUpSwipe:(UISwipeGestureRecognizer *)recognizer
{
[self.sineWave animateWave];
[self showSine];
[UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveLinear animations:^
{
self.sineWave.frame = CGRectOffset(self.sineWave.frame, 0, -246);
}
completion:^(BOOL finished)
{
NSLog(#"sine wave y position AFTER UP swipe %f", self.sineWave.frame.origin.y);
}];
}
- (IBAction)handleDownSwipe:(UISwipeGestureRecognizer *)recognizer
{
[self hideSine];
if(self.panedView.frame.origin.y <0)
{
[UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveLinear animations:^
{
self.sineWave.frame = CGRectOffset(self.sineWave.frame, 0, 246);
}
completion:^(BOOL finished)
{
}];
}
}
-(void) showSine
{
[self.sineWave setHidden:NO];
[self.sineWave fadeInAnimation];
}
-(void) hideSine
{
[self.sineWave fadeOutAnimation];
[self.sineWave setHidden:YES];
}
-(void) appDidEnterForeground:(NSNotification *)notification
{
[self.sineWave animateWave];
}
My sine wave is a subclass of UIView. This is the code within the sineWave class
- (void)animateWave {
[self.layer removeAllAnimations];
self.transform=CGAffineTransformIdentity;
[UIView animateWithDuration:3 delay:0.0 options: UIViewAnimationOptionRepeat | UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction animations:^{
self.transform = CGAffineTransformMakeTranslation(self.frame.size.width/2,0);
} completion:^(BOOL finished) {
self.transform = CGAffineTransformMakeTranslation(-self.frame.size.width/2, 0);
}];
}
-(void) fadeInAnimation
{
[UIView animateWithDuration:0 animations:^{
self.alpha = 1.0;}];
}
-(void) fadeOutAnimation
{
[UIView animateWithDuration:3 animations:^{
self.alpha = 0.0;}];
}
- (void)drawRect:(CGRect)rect
{
self.yc = 30;//The height of a crest.
float w = 0;//starting x value.
float y = rect.size.height;
float width = rect.size.width;
int cycles = 6;//number of waves
self.x = width/cycles;
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGContextSetLineWidth(context, 2);
while (w <= width) {
CGPathMoveToPoint(path, NULL, w,y/2);
CGPathAddQuadCurveToPoint(path, NULL, w+self.x/4, y/2 - self.yc, w+self.x/2, y/2);
CGPathAddQuadCurveToPoint(path, NULL, w+3*self.x/4, y/2 + self.yc, w+self.x, y/2);
w+=self.x;
}
CGContextAddPath(context, path);
[[UIColor colorWithRed:107/255.0f green:212/255.0f blue:231/255.0f alpha:1.0f]setStroke];
CGContextDrawPath(context, kCGPathStroke);
self.alpha= 0.0;
}
I was able to get this working thanks to a clue from this answer
In the view controller you need to subscribe to UIApplicationWillEnterForegroundNotification to get a notification when you app is returning from the background. I changed the viewDidLoad in the view controller and added a method to handle the notification -
- (void)viewDidLoad
{
[super viewDidLoad];
[self setUpSine];
[self.sineWave animateWave];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
-(void) appDidEnterForeground:(NSNotification *)notification
{
[self.sineWave animateWave];
}
I also removed the call to animateWave from the handleUpSwipe: method
- (IBAction)handleUpSwipe:(UISwipeGestureRecognizer *)recognizer
{
[self showSine];
[UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveLinear animations:^
{
self.sineWave.frame = CGRectOffset(self.sineWave.frame, 0, -246);
}
completion:^(BOOL finished)
{
NSLog(#"sine wave y position AFTER UP swipe %f", self.sineWave.frame.origin.y);
}];
}
You also need to make a change to animateWave to clear any previous animation before the animation is (re)started. You don't need anything in the completion block -
- (void)animateWave {
[self.layer removeAllAnimations];
self.transform=CGAffineTransformIdentity;
[UIView animateWithDuration:3 delay:0.0 options: UIViewAnimationOptionRepeat | UIViewAnimationOptionCurveLinear |UIViewAnimationOptionAllowUserInteraction animations:^{
self.transform = CGAffineTransformMakeTranslation(self.frame.size.width/2,0);
} completion:^(BOOL finished) {
}];
}

Creating a continuous animation

I want to create an animation that moves up or down the screen according to how fast the user taps the screen. The problem I am having is that I don't know how to create an infinite loop so I am firing a timer which presents issues. Here is my current code.
-(void)setPosOfCider {
CGFloat originalY = CGRectGetMinY(cider.frame);
float oY = originalY;
float posY = averageTapsPerSecond * 100;
float dur = 0;
dur = (oY - posY) / 100;
[UIImageView animateWithDuration:dur animations:^(void) {
cider.frame = CGRectMake(768, 1024 - posY, 768, 1024);
}];
}
Suggested fix(Doesn't work):
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
scroll.pagingEnabled = YES;
scroll.scrollEnabled = YES;
scroll.contentSize = CGSizeMake(768 * 3, 1024); // 3 pages wide.
scroll.delegate = self;
self.speedInPointsPerSecond = 200000;
self.tapEvents = [NSMutableArray array];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self startDisplayLink];
}
-(IBAction)tapped {
[self.tapEvents addObject:[NSDate date]];
// if less than two taps, no average speed
if ([self.tapEvents count] < 1)
return;
// only average the last three taps
if ([self.tapEvents count] > 2)
[self.tapEvents removeObjectAtIndex:0];
// now calculate the average taps per second of the last three taps
NSDate *start = self.tapEvents[0];
NSDate *end = [self.tapEvents lastObject];
self.averageTapsPerSecond = [self.tapEvents count] / [end timeIntervalSinceDate:start];
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.lastTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (CGFloat)yAxisValueBasedUponTapsPerSecond
{
CGFloat y = 1024 - (self.averageTapsPerSecond * 100.0);
return y;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval now = CACurrentMediaTime();
CGFloat elapsed = now - self.lastTime;
self.lastTime = now;
if (elapsed <= 0) return;
CGPoint center = self.cider.center;
CGFloat destinationY = [self yAxisValueBasedUponTapsPerSecond];
if (center.y == destinationY)
{
// we don't need to move it at all
return;
}
else if (center.y > destinationY)
{
// we need to move it up
center.y -= self.speedInPointsPerSecond * elapsed;
if (center.y < destinationY)
center.y = destinationY;
}
else
{
// we need to move it down
center.y += self.speedInPointsPerSecond * elapsed;
if (center.y > destinationY)
center.y = destinationY;
}
self.cider.center = center;
}
An even simpler approach is to employ the UIViewAnimationOptionBeginFromCurrentState option when using block-based animations:
[UIView animateWithDuration:2.0
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
animations:^{
// set the new frame
}
completion:NULL];
That stops the current block-based animation and starts the new one from the current location. Note, effective iOS 8 and later, this is now the default behavior, generally obviating the need for the UIViewAnimationOptionBeginFromCurrentState altogether. In fact, the default behavior considers not only the position of the view in question, but also the current velocity, ensuring a smooth transition. See WWDC 2014 video Building Interruptible and Responsive Interactions for more information.)
Another approach would be to have your taps (which adjust the averageTapsPerSecond) to stop any existing animation, grab the view's current frame from the presentation layer (which reflects the state of the view mid-animation), and then just start a new animation:
// grab the frame from the presentation layer (which is the frame mid-animation)
CALayer *presentationLayer = self.viewToAnimate.layer.presentationLayer;
CGRect frame = presentationLayer.frame;
// stop the animation
[self.viewToAnimate.layer removeAllAnimations];
// set the frame to be location we got from the presentation layer
self.viewToAnimate.frame = frame;
// now, starting from that location, animate to the new frame
[UIView animateWithDuration:3.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
self.viewToAnimate.frame = ...; // set the frame according to the averageTapsPerSecond
} completion:nil];
In iOS 7, you would use the rendition of animateWithDuration with the initialSpringVelocity: parameter, to ensure a smooth transition from the object's current movement to the new animation. See the aforementioned WWDC video for examples.
Original answer:
To adjust a view on the basis of the number of taps per second, you could use a CADisplayLink, which is like a NSTimer, but ideally suited for animation efforts like this. Bottom line, translate your averageTapsPerSecond into a y coordinate, and then use the CADisplayLink to animate the moving of some view to that y coordinate:
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval lastTime;
#property (nonatomic) CGFloat speedInPointsPerSecond;
#property (nonatomic, strong) NSMutableArray *tapEvents;
#property (nonatomic) CGFloat averageTapsPerSecond;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.speedInPointsPerSecond = 200.0;
self.tapEvents = [NSMutableArray array];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self.view addGestureRecognizer:tap];
}
- (void)handleTap:(UITapGestureRecognizer *)gesture
{
[self.tapEvents addObject:[NSDate date]];
// if less than two taps, no average speed
if ([self.tapEvents count] < 2)
return;
// only average the last three taps
if ([self.tapEvents count] > 3)
[self.tapEvents removeObjectAtIndex:0];
// now calculate the average taps per second of the last three taps
NSDate *start = self.tapEvents[0];
NSDate *end = [self.tapEvents lastObject];
self.averageTapsPerSecond = [self.tapEvents count] / [end timeIntervalSinceDate:start];
}
- (CGFloat)yAxisValueBasedUponTapsPerSecond
{
CGFloat y = 480 - self.averageTapsPerSecond * 100.0;
return y;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self startDisplayLink];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self stopDisplayLink];
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.lastTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval now = CACurrentMediaTime();
CGFloat elapsed = now - self.lastTime;
self.lastTime = now;
if (elapsed <= 0) return;
CGPoint center = self.viewToAnimate.center;
CGFloat destinationY = [self yAxisValueBasedUponTapsPerSecond];
if (center.y == destinationY)
{
// we don't need to move it at all
return;
}
else if (center.y > destinationY)
{
// we need to move it up
center.y -= self.speedInPointsPerSecond * elapsed;
if (center.y < destinationY)
center.y = destinationY;
}
else
{
// we need to move it down
center.y += self.speedInPointsPerSecond * elapsed;
if (center.y > destinationY)
center.y = destinationY;
}
self.viewToAnimate.center = center;
}
#end
Hopefully this illustrates the idea.
Also, to use the above, make sure you've added the QuartzCore framework to your project (though in Xcode 5, it seems to do that for you).
For standard, constant speed animation, you can use the following:
You would generally do this with an animation that repeats (i.e. UIViewAnimationOptionRepeat) and that autoreverses (i.e. UIViewAnimationOptionAutoreverse):
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
animations:^{
cider.frame = ... // specify the end destination here
}
completion:nil];
For a simple up-down-up animation, that's all you need. No timers needed.
There is one line you need to add to Rob's answer.
[UIView animateWithDuration:dur delay:del options:UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
[UIView setAnimationRepeatCount:HUGE_VAL];
//Your code here
} completion:nil];
If you need to stop that animation, use this line (remember to add QuartzCore framework):
[view.layer removeAllAnimations];

My animatewithduration, completion block is performed only once

I use the following block of code to slide a UIView down and when finished rotate another UIView.
The second part of the animation, the completion block is only performed once which means the 1st animation is not completed else it would reach the completion block.
On the iphone simulator it looks as if the 1st animation did finish...
can anyone help me figure this out?
my NSLog says:
finished 1st
started 2nd
finished 1st
finished 1st
finished 1st
.
.
.
- (IBAction) move
{
[UIView animateWithDuration:0.7 animations:^{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.7];
[UIView setAnimationRepeatCount:1];
[UIView setAnimationRepeatAutoreverses:NO];
CGPoint pos = movingtTable.center;
float moveDistance = 220.0;
if(!isViewVisible){
//expose the view
pos.y = pos.y+moveDistance;
//disable selection for xy table
xTable.userInteractionEnabled = NO;
yTable.userInteractionEnabled = NO;
//angle = M_PI;
}
else
{
pos.y = pos.y-moveDistance;
xTable.userInteractionEnabled = YES;
yTable.userInteractionEnabled = YES;
//angle = -M_PI;
}
isViewVisible = !isViewVisible;
movingtTable.center = pos;
NSLog(#"finished 1st");
}completion:^(BOOL finished){
NSLog(#"started 2nd");
[UIView animateWithDuration:0.4 animations:^{
//[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.4];
//[UIView setAnimationRepeatCount:1];
//[UIView setAnimationRepeatAutoreverses:NO];
arrows.transform = CGAffineTransformMakeRotation(angle);
}completion:^(BOOL finished){
angle = -angle;
}];
}];
Why are you trying to initialize another UIView animation inside the animateWithDuration block code? Update your code to the following and make sure you're not performing multiple animations of a single view at a time.
- (IBAction) move
{
[UIView animateWithDuration:0.7 animations:^{
CGPoint pos = movingtTable.center;
float moveDistance = 220.0;
if(!isViewVisible){
//expose the view
pos.y = pos.y+moveDistance;
//disable selection for xy table
xTable.userInteractionEnabled = NO;
yTable.userInteractionEnabled = NO;
//angle = M_PI;
}
else
{
pos.y = pos.y-moveDistance;
xTable.userInteractionEnabled = YES;
yTable.userInteractionEnabled = YES;
//angle = -M_PI;
}
isViewVisible = !isViewVisible;
movingtTable.center = pos;
NSLog(#"finished 1st");
}
completion:^(BOOL finished){
NSLog(#"started 2nd");
[UIView animateWithDuration:0.4 animations:^{
arrows.transform = CGAffineTransformMakeRotation(angle);
}completion:^(BOOL finished){
angle = -angle;
}];
}];
BTW: The block code requires some serious refactoring, if you ask me :)
You are mixing and matching paradigms and I believe that is causing the issue you are seeing. You are creating an animation block, but inside of that block you are creating a new animation routine with the 'old' paradigm for running UIView animations. Apple is leading people away from the old paradigm and I would encourage you to ONLY use blocks as well.
This is why the completion block only runs once, the UIView animateWith block code only runs once. However, your internal animation code runs multiple times.
Take out:
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.7];
[UIView setAnimationRepeatCount:1];
[UIView setAnimationRepeatAutoreverses:NO];
If you want your animation block to run several times, then use the full method:
animateWithDuration:delay:options:animations:completion:
Make the delay = 0, and set your options to UIViewAnimationOptionRepeat, or whatever you need to accomplish the number of cycles you want the block to complete.
Here is my suggestion assuming you want it to repeat:
- (IBAction) move
{
[UIView animateWithDuration:0.7
delay:0
options:UIViewAnimationOptionRepeat
animations:^{
CGPoint pos = movingtTable.center;
float moveDistance = 220.0;
if(!isViewVisible) {
//expose the view
pos.y = pos.y+moveDistance;
//disable selection for xy table
xTable.userInteractionEnabled = NO;
yTable.userInteractionEnabled = NO;
//angle = M_PI;
}
else {
pos.y = pos.y-moveDistance;
xTable.userInteractionEnabled = YES;
yTable.userInteractionEnabled = YES;
//angle = -M_PI;
}
isViewVisible = !isViewVisible;
movingtTable.center = pos;
NSLog(#"finished 1st");
}
completion:^(BOOL finished){
NSLog(#"started 2nd");
[UIView animateWithDuration:0.4
animations:^{
arrows.transform = CGAffineTransformMakeRotation(angle);
}
completion:^(BOOL finished){
angle = -angle;
}];
}];
}

Resources