How to Animate Images in a UIImageView with Completion Handler

I needed to do a simple animation on my iOS project recently, and I was frustrated by how difficult it turned out to be. I wasn’t writing a game, so I didn’t need to bring in the power of the Sprite Kit libraries. All I needed was to show a series of images in a UIImageView.  

My first attempt was to use the UIImageView.animationImages api to do my animation. This was incredibly easy, but very limiting. After the animation was complete, I needed to show the last frame of the animation in the UIImageView. This proved to be very difficult if not impossible with the animationImages API. After the animation completes, the UIImageView is reset to the original image that was displayed before the animation started. If my animation had been symmetric, it would not have been a problem.

Attempt #1 – Using the AnimationImages Property for Animation

This API is really simple. Load up a series of images in an array and set the UIImageView.animationImages property to that array of images. Next tell the UIImageView how long your animation is and whether to repeat the animation and call start. It is that easy.

NSMutableArray *images = [[NSMutableArray alloc] init];
NSInteger animationImageCount = 38;
for (int i = 0; i < animationImageCount; i++) {
    [images addObject:[UIImage imageNamed:[NSString stringWithFormat:@"IndexedImagesInMyAnimation%d", i]]];
}

self.animationImageView.animationImages = images;
self.animationImageView.animationDuration = animationImageCount / 24.0;
self.animationImageView.animationRepeatCount = 1;
[self.animationImageView startAnimating];

The problem is there is no completion block or delegate protocol for getting notified when the animation is complete. Therefore I could not reset the UIImageView to the last image in the animation.

A quick search on Stack Overflow didn’t turn up any good solutions. The only hack was to call performSelector with a delay that matched the duration of my animation. This did not work for me because there was a lag in starting the animation, so I couldn’t get the timing just right. I either showed the last frame of the animation too early or the image reset to the first frame, for a brief moment, and then back to the last frame.

Attempt #2 – Using CAKeyFrameAnimation and animationDidStop

My second attempt was to use the CAKeyFrameAnimation API. This gave me much more control over my animation. I could get notified when the animation was completed by setting the delegate of the animation, and the animation would call animationDidStop when it was finished.

I saved the last frame of my animation so I could set the UIImageView when the animation was finished. I also had to change my NSArray of images to store the CGImage instead of the UIImage.

- (void) playAnimation {
    NSMutableArray *images = [[NSMutableArray alloc] init];
    NSInteger animationImageCount = 38;
    for (int i = 0; i < animationImageCount; i++) {
        // Images are numbered IndexedImagesInMyAnimation0, 1, 2, etc...
        [images addObject:(id)[UIImage imageNamed:[NSString stringWithFormat:@"IndexedImagesInMyAnimation%d", i]].CGImage];
    }
    self.lastImageInAnimation = [UIImage imageNamed:[NSString stringWithFormat:@"IndexedImagesInMyAnimation%d", animationImageCount - 1]];

    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
    animation.calculationMode = kCAAnimationDiscrete;
    animation.duration = animationImageCount / 24.0; // 24 frames per second
    animation.values = images;
    animation.repeatCount = 1;
    animation.delegate = self;
    [self.animationImageView.layer addAnimation:animation forKey:@"animation"];
}

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
    if (flag) {
        self.animationImageView.image = self.lastImageInAnimation;
    }
}

This worked okay, but with further exploration of CAKeyFrameAnimation API, I discovered a way to keep the last frame of the animation displayed. I didn’t even need the animationDidStop function anymore. By setting the poorly-named fillMode property appropriately, I could keep the animation visible after it was complete.

Attempt #3 – Using CAKeyFrameAnimation and kCAFillModeForwards to Keep the Last Frame Visible

NSMutableArray *images = [[NSMutableArray alloc] init];
NSInteger animationImageCount = 38;
for (int i = 0; i < animationImageCount; i++) {
    // Images are numbered IndexedImagesInMyAnimation0, 1, 2, etc...
    [images addObject:(id)[UIImage imageNamed:[NSString stringWithFormat:@"IndexedImagesInMyAnimation%d", i]].CGImage];
}

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
animation.calculationMode = kCAAnimationDiscrete;
animation.duration = animationImageCount / 24.0; // 24 frames per second
animation.values = images;
animation.repeatCount = 1;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[self.animationImageView.layer addAnimation:animation forKey:@"animation"];

Wondering what other values you could set for fillMode? Here is a complete list of possible values. The receiver in this case is the animation.

  • kCAFillModeRemoved – The receiver does not appear until it begins and is removed from the presentation when it is completed.
  • kCAFillModeForwards – The receiver does not appear until it begins but remains visible in its final state when it is completed.
  • kCAFillModeBackwards – The receiver appears in its initial state before it begins but is removed from the presentation when it is completed.
  • kCAFillModeBoth – The receiver appears in its initial state before it begins and remains visible in its final state when it is completed.

The AnimationImages method has so much promise as a nice simple way of animating images, but in the end I needed more control. Luckily Apple has provided us CAKeyFrameAnimation to fill that role.
 

Conversation
  • Alex says:

    Thanks!
    Just found a useful github repo using GCD and UIImageView
    https://github.com/gurmundi7/UIImageView-AnimationCompletionBlock

  • Selim Bakdemir says:

    If your concern is only reset to an image frame when the animation finished, you can achieve this by setting that frame to the .image property.

    UIImageView iterates over the animatedImages , when animation finished it just returns back to the previously set .image property, which will be your last image frame or whatever.

    • Mike Woelmer Mike Woelmer says:

      Hi Selim,

      I did not want to reset the the image property to the previously set image. I wanted it to be the last frame of my animation.

      • KimS says:

        You don’t have to do any of that additional leg work you know.

        You get that behaviour for free, if you use a repeatCount of 1 and set the animationImages array to your animation images and then subsequently set the image field of the UIImageView to the last item in your animationImages array.

        .animationImages = images
        .animationDuration = duration
        .animationRepeatCount = repeatCount
        .image = images.last

        The image fields behaviour is to be invisible until the animation completes, then it appears so if you set it to your last frame then it will be the last frame when the animation finishes.

        We do this all the time, its really basic and free behaviour that you get in there.

  • Juliana says:

    How could I share an animation to a social netowork as a video? is there a way to get a video out of that?

  • Mike says:

    Great post! Thanks for the useful information. I had a very similar situation. I had an animation that I had build up from a sequences of jpeg images. As you found, the animationImages property of UIImageView is very easy to use but not very powerful. I want to be able to tap on the image to pause it, and pan left and right to go backwards and forwards in my animation. And I don’t want to have to use all the complexity of Sprite Kit to do it. I’m going take a look at CAKeyFrameAnimation and see if it will provide a solution for me.

  • Carlos Pinto says:

    Hi Mike, Thanks for your detailed post.
    I ran into the same exact issue, wanting to stop the animation at the last frame. I have tried your solution, except in Swift, but so far no luck. I am trying the #2 trial in your post to see that the animation actually ran and ended, however, I never see the animation actually running.

    This is the code that I am testing with at the moment:

    func setUpAnimation() {

    endImage = UIImage(named: "whiskeyAnimation_06")!

    var images : [UIImage] = []
    var animationImageCount = 6
    var imageName : String
    var image : UIImage

    for (var i = 1; i <= animationImageCount; i++) {
    imageName = "testImage_0\(i)"
    image = UIImage(named: imageName)!
    images.append(image)
    }

    animation = CAKeyframeAnimation(keyPath: "contents")

    animation.calculationMode = kCAAnimationDiscrete;
    animation.duration = 1
    animation.values = images
    animation.repeatCount = 1
    animation.removedOnCompletion = false
    animation.fillMode = kCAFillModeForwards;
    animation.delegate = self
    layer.addAnimation(animation, forKey: "animation")
    }

    override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
    image = endImage
    }

    Any help is greatly appreciated.
    -Carlos

    • Myunkyu Park says:

      You should use CGI image instead of UIImage.

  • gabriel says:

    what is my problem?? when I am clicking button nothing happening, help me please

    import QuartzCore

    @IBAction func buttonClicked(sender: UIButton){
    let animation: CAKeyframeAnimation = CAKeyframeAnimation(keyPath: “contents”)
    animation.calculationMode = kCAAnimationDiscrete;
    animation.values = imageList
    animation.duration = 1
    animation.repeatCount = 1;
    animation.removedOnCompletion = false
    animation.fillMode = kCAFillModeForwards;
    self.imageSeq.layer.addAnimation(animation, forKey: “animation”)
    }

    imageList – is image array (this array working perfectly on animationImages)
    imageSeq – is UIImageview

  • Comments are closed.