Chain UIView animations with time intervals - iphone

I need to animate 3 UIViews (fade in/out).
1 animation duration is 0.6s (fade in/out cycle is 0.6+0.6s). But I need to launch animations in 0.2 seconds.
1st animation should be launched in 0.0 seconds.
2nd animation should be launched in 0.2 seconds.
3rd animation should be launched in 0.4 seconds.
And all of them should be looped "indefinitely" (until some trigger).
What I have at the moment:
- (void)playAnimation {
isAnimated = YES;
[self animateView:firstView afterDelay:0.0];
[self animateView:secondView afterDelay:0.2];
[self animateView:thirdView afterDelay:0.4];
}
- (void)stopAnimation {
isAnimated = NO;
}
- (void)animateView:(UIView *)animatedView afterDelay:(float)delay {
if(isAnimated) {
[UIView animateWithDuration:0.6 delay:delay options:UIViewAnimationOptionTransitionNone
animations:^ {
animatedView.alpha = 1.0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.6 animations:^ {
animatedView.alpha = 0.0;
} completion:^(BOOL finished) {
[self animateView:animatedView afterDelay:0.0];
}];
}];
}
}
This code works unpredictable. Sometimes view animation works like I want (with phase 0.2 seconds), some times it starts in the same time...
What will be the proper way to do that? I've also tried to remove afterDelay: part from method signature and launch animateView method like that with exactly same effect:
[self performSelector:#selector(animateView:) withObject:thirdView afterDelay:0.6];
UPDATE
I've noticed that animation "breaks" when heavy networking stuff is performing in background (loading big images using AFNetworking).
I don't mind if animation will "freeze" a bit (though I prefer to not have delays at all) but I really want to keep phases of all animations linked (with same phase difference).
To make problem easier to understand I've added graphs. Y is alpha, X is time. Top 3 graphs - what I want to have. Bottom ones - what I currently have. Highlighted area is where problem comes. You can see that second view's animation freeze for 0.2 seconds and synchronise with 3rd one. So they start blinking in the same phase. This is just one example. Some times they can animate ok, sometimes all 3 views "syncronize" in few rounds of animation and blink in same phase...

Looks like you want the same animation, applied to all 3 views, offset by t=0.2. You can use Core Animation to do exactly what you want with very little effort.
Doing it this way they will always be timed correctly.
I propose this:
-(void)playAnimation
{
CABasicAnimation * anim = [ CABasicAnimation animationWithKeyPath:#"opacity" ] ;
anim.autoreverses = YES ;
anim.repeatCount = CGFLOAT_MAX ;
anim.removedOnCompletion = NO ;
anim.duration = 0.6 ;
anim.fromValue = #0.0 ;
anim.toValue = #1.0;
// finish configuring your animation here (timing function, speed, duration, fill mode, etc) ...
CFTimeInterval t = CACurrentMediaTime() ;
anim.beginTime = t ;
[ self.firstView.layer addAnimation:anim forKey:#"opacity-anim" ] ; // name is so you can remove this anim later
anim.beginTime += 0.2 ;
[ self.secondView.layer addAnimation:anim forKey:#"opacity-anim" ] ;
anim.beginTime += 0.2 ;
[ self.thirdView.layer addAnimation:anim forKey:#"opacity-anim" ] ; // name is so you can remove this anim later
}
-(void)stopAnimation
{
[ self.firstView.layer removeAnimationForKey:#"opacity-anim" ] ;
[ self.secondView.layer removeAnimationForKey:#"opacity-anim" ] ;
[ self.thirdView.layer removeAnimationForKey:#"opacity-anim" ] ;
}
edit: oops! forgot the start, end values!

The way to schedule animations properly is by using the CAMediaTiming protocol that the CAKeyframeAnimation class conforms to. See my answer below for links to resources on how to achieve this.
Changing speed of an ongoing CAKeyframeAnimation animation

Try instead of performSelector: the following sample
- (void)animateView:(UIView *)animatedView afterDelay:(float)delay {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
if(isAnimated) {
[UIView animateWithDuration:0.6 delay:0.0 options:UIViewAnimationOptionTransitionNone
animations:^ {
animatedView.alpha = 1.0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.6 animations:^ {
animatedView.alpha = 0.0;
} completion:^(BOOL finished) {
[self animateView:animatedView afterDelay:0.0];
}];
}];
}
});
}

Hope it will work as what you want.
- (void)animateView:(UIView *)animatedView afterDelay:(float)delay
{
if(isAnimated) {
[UIView animateWithDuration:0.6 delay:delay options:UIViewAnimationOptionTransitionNone
animations:^ {
animatedView.alpha = 1.0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.6 delay:delay options:UIViewAnimationOptionTransitionNone animations:^{
animatedView.alpha = 0.0;
} completion:^(BOOL finished) {
[self animateView:animatedView afterDelay:delay];
}];
}];
}
}
This is just some modification of your code.

Try this, this might work for you.
- (void)playAnimation {
isAnimated = YES;
[self performSelector:#selector(animateView:) withObject:firstView afterDelay:0.1];
[self performSelector:#selector(animateView:) withObject:secondView afterDelay:0.2];
[self performSelector:#selector(animateView:) withObject:thirdView afterDelay:0.4];
}
- (void)animateView:(UIView *)animatedView {
//Here "animatedView" will contain (firstView/secondView/thirdView) whatever you are passing
//your animation
}

Related

UIPercentDrivenInteractiveTransition cancelTransition turns screen black

I'm implementing custom navigation transitions, and using UIPercentDrivenInteractiveTransition to do the trick.
I've got the noninteractive transition working fine, and I'm using an animation block to handle the transition animation, but once I added in interactive transitions, the screen now goes black if I cancel the transition.
Here's my code that tracks the gesture:
-(void)userDidPan:(UIScreenEdgePanGestureRecognizer *)recognizer {
CGPoint location = [recognizer locationInView:nil];
if (recognizer.state == UIGestureRecognizerStateBegan) {
// We're being invoked via a gesture recognizer – we are necessarily interactive
self.hasActiveInteraction = YES;
self.interactiveTransition = [BXTPercentDrivenInteractiveTransition new];
[[BXTNavigationController sharedNavigationController] popViewControllerAnimated:YES];
}
else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGFloat ratio = location.x / CGRectGetWidth(recognizer.view.frame);
NSLog(#"Percentage complete: %0.2f",ratio*100);
[self.interactiveTransition updateInteractiveTransition:ratio];
}
else if (recognizer.state == UIGestureRecognizerStateEnded) {
// Depending on our state and the velocity, determine whether to cancel or complete the transition.
CGFloat ratio = location.x / CGRectGetWidth(recognizer.view.frame);
if (ratio > 0.50) {
NSLog(#"Completing");
[self.interactiveTransition finishInteractiveTransition];
}
else {
NSLog(#"Canceling");
[self.interactiveTransition cancelInteractiveTransition];
}
}
}
...and here's an (abbreviated) look at my animation block when popping off the view controller:
[UIView animateWithDuration:kBXTNavigationTimingDuration
delay:0
options:animationOption
animations:^{
toVC.view.frame = destinationFrameForPoppedView;
fromVC.view.frame = CGRectOffset(fromVC.view.frame, fromVC.view.frame.size.width, 0);
} completion:^(BOOL finished) {
[darkeningView removeFromSuperview];
[_transition.secondaryTransitionViews enumerateObjectsUsingBlock:^(BXTTransitionView *transitionView, NSUInteger idx, BOOL *stop) {
[transitionView.view removeFromSuperview];
}];
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
Any ideas why the screen goes black when cancelling a swipe-from-the-left pop animation with a custom navigation transition?
Solved! I deleted all the removeFromSuperview calls in the animation completion block and it solved it (I had the same problem like you with almost the same code, see my comment to your q'). Try removing these lines from your code:
[darkeningView removeFromSuperview];
[_transition.secondaryTransitionViews enumerateObjectsUsingBlock:^(BXTTransitionView *transitionView, NSUInteger idx, BOOL *stop) {
[transitionView.view removeFromSuperview];
}];

Transform resets(redraws) all my former moved views

I'm moving around image views with timers and animation blocks. At the same time a user can click on a few of these views(buttons) and then I have some more animation blocks that start moving/resizing/rotating different image views.
Changing alpha of views and giving out new CGpoint to views works very well for me but calling resizing or rotating with the uiview.transform function seems to reset all my views to their starting position(from the storyboard).
I really would love to be able to use this transform function inside an animation block but if I can't somehow bypass this reset it is not really a viable option for me.
example code:
- (void)moveGust:(NSTimer*) timer
{
if(centerCloud1.x >= 1100)
{
centerCloud1.x = -79;
gustOutlet.center = centerCloud1;
}
if(centerCloud2.x >= 1100)
{
centerCloud2.x = -79;
cloudOutlet.center = centerCloud2;
}
centerCloud1 = gustOutlet.center;
centerCloud1.x = round(centerCloud1.x+10);
centerCloud2 = cloudOutlet.center;
centerCloud2.x = round(centerCloud2.x+8);
[UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear animations:^(void){
gustOutlet.center = centerCloud1;
cloudOutlet.center = centerCloud2;
}completion:^(BOOL Finished){ }];
}
- (IBAction)signAction:(id)sender {
[UIView animateWithDuration:0 animations:^(void) {
signTop.transform = CGAffineTransformScale(signTop.transform, 1.1, 1.1);
}];
}
Any ideas?
Thanks!
Successive transforms need to be concatenated using CGAffineTransformConcat().
When you set the view's transform equal to a new transform, you effectively "undo" previous transforms. To do a series of transforms, concatenate them with the above method.

CGRect not changing during animation

I'm animating an UIView and want to check if its frame intersects with another UIView's frame. This is how I "spawn" one of the UIViews:
- (void) spawnOncomer
{
oncomer1 = [[Oncomer alloc] initWithType:#"car"];
[self.view addSubview:oncomer1];
//make the oncomer race across the screen
[UIView beginAnimations:nil context:NULL]; {
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:3.0];
[UIView setAnimationDelegate:self];
CGRect f = oncomer1.frame;
f.origin.y = self.view.frame.size.height+oncomer1.frame.size.height;
oncomer1.frame = f;
[UIView setAnimationDidStopSelector:#selector(decountCar)];
}
[UIView commitAnimations];
}
So far so good. Now I want to check if this UIView and my other UIView collide by doing this:
- (void) checkCollision {
bool collision = CGRectIntersectsRect(car.frame, oncomer1.frame);
if (collision) {
NSLog(#"BOOOOOOOM");
} else {
NSLog(#"Oncomer: %#", NSStringFromCGRect(oncomer1.frame));
}
}
However, they never collide. Although I see oncomer1 moving across the screen, loggin oncomer1.frame never changes: it keeps outputting Oncomer: {{50, 520}, {30, 60}}(which are the post-animation values).
Does anyone know why this is?
P.s. Both methods are called directly or indirectly with a NSTimer and are thus performed in the background
UIView geometry updates apply immediately to their CALayer, even in an animation block. To get a version of a layer with animations applied, you can use -[CALayer presentationLayer], like this (warning - untested code):
- (void) checkCollision {
CGRect oncomerFrame = oncomer1.layer.presentationLayer.frame;
bool collision = CGRectIntersectsRect(car.frame, oncomerFrame);
if (collision) {
NSLog(#"BOOOOOOOM");
} else {
NSLog(#"Oncomer: %#", NSStringFromCGRect(oncomerFrame));
}
}
From your code:
CGRect f = oncomer1.frame;
f.origin.y = self.view.frame.size.height+oncomer1.frame.size.height;
oncomer1.frame = f;
A logical explanation of the frame never changing is that you are only changing the y of the frame, and you are always setting it to the same value determined by two heights.

Repeat an animation a variable number of times

I was wondering how I can set an animation to repeat. The number of repetitions needs to be determined by a variable. In the following code, the variable int newPage should determine how often the animation is repeated.
I tried this, but the animation (which employs a block animation) was only executed once:
for (int temp = 1; temp <= newPage; temp++) {
[self animatePage];
}
If I code the following, it works like I want it to, but this is hardcoded (i.e. the animation will be repeated twice) and I can't see a way of how to change the number of how often this animation is executed in code and according to my variable newPage:
[UIView animateWithDuration:0
delay:0.1
options:UIViewAnimationOptionCurveEaseIn
animations:^{[self animatePage];}
completion:^(BOOL finished){[self animatePage];}];
I'd be very grateful for suggestions of how to repeat the same animation without having to hardcode the number of times I want this animation to be repeated.
EDIT:
I tried to implement the following code, but only one animation will actually be carried out:
[UIView animateWithDuration:0
delay:1
options:UIViewAnimationOptionCurveEaseIn
animations:^{
[UIView setAnimationRepeatCount:2];
[self animatePage];
}
completion:nil];
Had the same problem - you were missing an an'UIViewAnimationOptionRepeat'
This should work:
[UIView animateWithDuration:0
delay:1
options:UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionRepeat
animations:^{
[UIView setAnimationRepeatCount:2]; // **This should appear in the beginning of the block**
[self animatePage];
}
completion:nil];
Did the trick for me.
Did you try to set the repeatCount? + (void)setAnimationRepeatCount:(float)repeatCount
I've tried the following code block, and it definitely repeats 2x for me (l was a UITextView that was scaled up by 2x in X dir and 3X in Y dir):
[UIView animateWithDuration:2
delay:0.1
options:UIViewAnimationOptionCurveEaseIn
animations:^{ [UIView setAnimationRepeatCount:2];
l.transform = CGAffineTransformMakeScale(2,3); } completion:nil];
the reason you arent seeing but one animation is that due to the fact that you are calling the animation from a loop in the same runloop, resuting in the last call winning (one animation)
instead of calling [self animatePage], try calling
[self performSelector:#selector(animatePage) withObject:nil afterDelay:.1 *temp];
this will create your calls on separate threads.
you may need to play with the delay interval
To avoid the hiccup between animations, use keyframes. I wrote an extension that works on anything conforming to UIView (which is a ton of visual elements)
Swift 4:
import UIKit
extension UIView{
func rotate(count: Float, _ complete: #escaping ()->()) {
UIView.animateKeyframes(withDuration: 1.0, delay: 0, options: [.repeat], animations: {
//to rotate infinitely, comment the next line
UIView.setAnimationRepeatCount(count)
//rotate the object to 180 degrees
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5/1.0, animations: {
self.transform = CGAffineTransform(rotationAngle: (CGFloat(Double.pi)))
})
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5/1.0, animations: {
//rotate the object from 180 to 360 degrees
self.transform = CGAffineTransform(rotationAngle: (CGFloat(Double.pi * 2)))
})
}, completion:{ _ in
complete()
})
}
}
To call it anywhere in the app:
view.rotate() { /*do after*/ }

Performance issue with DrawRect and NSTimer

I'm trying to make a slot machine animation where the reels spin. to do this, I'm using drawRect to draw images in a custom class that inherits from UIView. I'm using an nstimer to update the position of the images and calling [self setNeedsDisplay] to update the drawing. In the simulator, it looks very good, however, on the device, it is very laggy. I was wondering if i'm doing something wrong with my method of drawing or is there any better solutions.
- (void)drawRect:(CGRect)rect
{
[image1 drawInRect:CGRectMake(0, image1Position, 98, 80)];
[image2 drawInRect:CGRectMake(0, image2Position, 98, 80)];
[image3 drawInRect:CGRectMake(0, image3Position, 98, 80)];
}
- (void)spin
{
// move each position down by 10 px
image1Position -= MOVEMENT;
image2Position -= MOVEMENT;
image3Position -= MOVEMENT;
// if any of the position <= -60 reset to 180
if(image1Position < -50)
{
image1Position = 180;
}
if(image2Position < -50)
{
image2Position = 180;
}
if(image3Position < -50)
{
image3Position = 180;
}
[self setNeedsDisplay];
}
-(void)beginSpinAnimation
{
timer = [NSTimer scheduledTimerWithTimeInterval:SCROLL_TIME target:self selector:#selector(spin) userInfo:self repeats:YES];
}
My CoreAnimation Attempt with UIScrollView:
- (void) spinToNextReel
{
int y = self.contentOffset.y + 80;
// if the current >= last element reset to first position (-30)
if(y >= (80 *(elementCount+1) - 30))
{
y = -30;
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:SCROLL_TIME];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
self.contentOffset = CGPointMake(0, y);
[UIView commitAnimations];
if (!isSpinning && targetY == y)
{
NSLog(#"target is %d, y is %d", targetY, y);
if(timer)
{
[timer invalidate];
timer = nil;
}
[self playSound];
}
}
I would say research into CoreAnimation. It is made to do what you want to do here. It'll be much faster than what you are doing here.
As for it being slow, calling drawInRect isn't the fastest thing in the world. What is SCROLL_TIME?
You want to use CoreAnimation, it will be a lot easier, and more efficient. Having said that, if you insist on trying to manually animate this way you are doing a couple of thing wrong:
Do not attempt move a constant amount on fixed intervals, timer events can be delayed, and if they are that will result in your animation being uneven, since you are moving a constant amount per event, not per time interval. You should record the actual timestamp every time you animate, compare it to the previous timestamp, and move an appropriate number of pixels. This will result in even amounts of movement even if the events are delayed (effectively you will being dropping frames).
Do not use an NSTimer, use a CADisplayLink. That will tie your drawing to the native refresh rate of the system, and synchronize it with any other drawing that is going on.
I know I said it already, but learn and use CoreAnimation, you will be much happier.