How to Make Uiview Animation Sequence Repeat and Autoreverse

How to make UIView animation sequence repeat and autoreverse

To animation from point 1 to 2 to 3 to 2 to 1 and repeat, you can do use animateKeyframesWithDuration in iOS 7 and later:

someView.frame = frame1;
[UIView animateKeyframesWithDuration:2.0 delay:0.0 options:UIViewKeyframeAnimationOptionAutoreverse | UIViewKeyframeAnimationOptionRepeat animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
someView.frame = frame2;
}];
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
someView.frame = frame3;
}];
} completion:nil];

If using auto-layout, you can animate the changing of the constraint constants:

[UIView animateKeyframesWithDuration:2.0 delay:0.0 options:UIViewKeyframeAnimationOptionAutoreverse | UIViewKeyframeAnimationOptionRepeat animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
topConstraint.constant = 200;
leftConstraint.constant = 200;
[self.view layoutIfNeeded];
}];
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
topConstraint.constant = 100;
leftConstraint.constant = 300;
[self.view layoutIfNeeded];
}];
} completion:nil];

Or, the approach with auto layout is to deactivate the constraints, and then you can animate using frame values or what have you.


In earlier versions of iOS, you can use CAKeyframeAnimation, for example to animate along a path:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(100.0, 100.0)];
[path addLineToPoint:CGPointMake(200.0, 200.0)];
[path addLineToPoint:CGPointMake(100.0, 300.0)];

CAKeyframeAnimation *animatePosition = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animatePosition.path = [path CGPath];
animatePosition.duration = 1.0;
animatePosition.autoreverses = YES;
animatePosition.repeatCount = HUGE_VALF;
[self.someView.layer addAnimation:animatePosition forKey:@"position"];

You can do this with however many points you want. This also useful technique if you want to animate along a curved path (e.g. a circle or bezier curve).


To just animate between two points, you can use animateWithDuration:delay:options:animations:completion:, such as:

[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat | UIViewAnimationOptionCurveEaseInOut
animations:^{
// do whatever animation you want, e.g.,

someView.frame = someFrame1;
}
completion:NULL];

This animates the movement of someView from the starting frame to someFrame1 and back.

By the way, using UIViewAnimationOptionCurveEaseInOut in conjunction with UIViewAnimationOptionAutoreverse and UIViewAnimationOptionRepeat will give you a smoother effect as the animation reverses and repeats.

Reversing a UIView animation and doing the opposite translationX movement

If you change your animation block slightly you should get the effect you are after:

// For the first animation, animate from 0 to -250 (no repeat)
UIView.animate(withDuration: 20, delay: 0, options: [ .curveEaseInOut], animations: {
self.imageView.transform = .init(translationX: -250, y: 0)

}, completion: { _ in
// Once that's done, begin a repeating animation between -250 and 250
UIView.animate(withDuration: 20, delay: 0, options: [.autoreverse, .repeat, .curveEaseInOut], animations: {
self.imageView.transform = .init(translationX: 250, y: 0)

}, completion: nil)
})

UIView.animateWithDuration swift loop animation

No need to do the completion block approach, just use the animation options argument:

updated for Swift 3.0

UIView.animate(withDuration: 2.0, delay: 0, options: [.repeat, .autoreverse], animations: {

coloredSquare.frame = CGRect(x: 120, y: 220, width: 100, height: 100)

}, completion: nil)

If for any reason you want to stop the animation later, just use:

coloredSquare.layer.removeAllAnimations()

How to sequence two animations with delay in between

You can perform the second animation in the completionHandler presented on UIView.animate

let duration = self.transitionDuration(using: transitionContext)

let firstAnimDuration = 0.5
UIView.animate(withDuration: firstAnimDuration, animations: {
/* Do here the first animation */
}) { (completed) in

let secondAnimDuration = 0.5
UIView.animate(withDuration: secondAnimDuration, animations: {
/* Do here the second animation */
})
}

Now you could have another problem.

If you rotate your view with the CGAffineTransform and for every animation you assign a new object of this type to your view.transform, you will lose the previous transform operation

So, according to this post: How to apply multiple transforms in Swift, you need to concat the transform operation

Example with 2 animation block

This is an example to made a rotation of 180 and returning back to origin after 1 sec:

let view = UIView.init(frame: CGRect.init(origin: self.view.center, size: CGSize.init(width: 100, height: 100)))
view.backgroundColor = UIColor.red
self.view.addSubview(view)

var transform = view.transform
transform = transform.rotated(by: 180)

UIView.animate(withDuration: 2, animations: {
view.transform = transform
}) { (completed) in

transform = CGAffineTransform.identity
UIView.animate(withDuration: 2, delay: 1, options: [], animations: {
view.transform = transform
}, completion: nil)
}

Sample Image

Example of .repeat animation and .autoreverse

The .animate method give you the ability to set some animation options. In particular the structure UIViewAnimationOptions contains:

  1. .repeat, which repeat indefinitely your animation block
  2. .autoreverse, which restore your view to the original status

With this in mind you could do this:

var transform = view.transform.rotated(by: 180)
UIView.animate(withDuration: 2, delay: 0, options: [.repeat, .autoreverse], animations: {
self.myView.transform = transform
})

But you need a delay between the two animations, so you need to do this trick:

Example of recursive animation and a delay of 1 sec

Just create a method inside your ViewController which animate your view. In the last completionHandler, just call the method to create a infinite loop.

Last you need to call the method on viewDidAppear to start the animation.

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

self.animation()
}

func animation() {
var transform = view.transform
transform = transform.rotated(by: 180)

UIView.animate(withDuration: 2, delay: 0, options: [], animations: {

self.myView.transform = transform

}) { bool in
transform = CGAffineTransform.identity

UIView.animate(withDuration: 2, delay: 1, options: [], animations: {

self.myView.transform = transform

}, completion: { bool in
self.animation()
})
}
}

UIView infinite loop Animation to call a method after every repeat cycle

OK, I have come a long way since I posted this question.

The accepted design pattern is to actually generate a clock tick (commonly using CADisplayLink) that runs at either 30 frames per second or 60 depending on how smooth and fast you want the animation to be. At every frame callback, you modify the transform on the view/layer and use a transaction to display it.

Some key notes:
Build a method to test for intersecting line and point:

-(BOOL)intersectWithAngle:(CGFloat)rad {        
return (rad <= angle && rad >= angle - angleIncrement);
}

You must use transactions when attempting to modify a property that is animatable to prevent the layer/view from animating the new value itself. (You don't want to animate it twice)

[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

self.transform = rotationTransform;

[CATransaction commit];

No need to keep creating new transforms every frame, this is just wasteful and consumes resources. Best create an instance variable / property called rotationTransform and modify that every frame and re-apply it to the layer/view.

-(void)displayFrame {   
rotationTransform.a = cos(angle);
rotationTransform.b = sin(angle);
rotationTransform.c = -sin(angle);
rotationTransform.d = cos(angle);

[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

self.transform = rotationTransform;

[CATransaction commit];
}

Next, create a method to actually set up the angle, use an instance variable / property to store the angle and define your angle incrementation (I use 3 degrees), also predefine 2-pi so that you are not always recalculating it.

-(void)processFrame {
angle += angleIncrement;
if (angle >= M_2PI) {
angle -= M_2PI;
}

if ([self intersectWithAngle:point.angle]) {
[point animate];
}
}

Finally, build your method that is called by CADisplayLink which sets up the frame and displays it while maintaining frame sync.

-(void)displayLinkDidTick {
// get the time now
NSTimeInterval timeNow = [NSDate timeIntervalSinceReferenceDate];

if (timeOfLastDraw == 0) {
numberOfTicks = 1;
} else {
NSTimeInterval timeSinceLastDraw = timeNow - timeOfLastDraw;
NSTimeInterval desiredTimeInterval = kFrameDuration;

numberOfTicks = (NSUInteger)(timeSinceLastDraw / desiredTimeInterval);
}

if (numberOfTicks > 4) {
numberOfTicks = 4;
timeOfLastDraw = timeNow;
} else {
timeOfLastDraw += numberOfTicks * kFrameDuration;
}

while(numberOfTicks--) {

[self processFrame];
if (spriteState == SpriteStateIdle)
break;
}

[self displayFrame];

}

This code extract has been heavily modified for this post, in actually fact I do the animation of the scan line and the blipping of the point in the same CADisplayLink instance.



Related Topics



Leave a reply



Submit