Play an animated GIF with an IKImageView

Hi, this is a small note for people wondering (like I was, a few days ago) how the hell can I animate my GIFs in an IKImageView !? Well, unlike the NSImageView, where you only have to set a property to YES to animate you GIFs, with the IKImageView, it’s a bit more complicated. You have to subclass your IKImageView, and set an overlay layer on the image, setting it yourself. In my image viewer application, the name of my subclass of IKImageView is ImageView (original isn’t it ? :p) So here is the code of ImageView handling the animation of GIFs, with comments :

#import "ImageView.h"

@implementation ImageView

-(void)awakeFromNib
{
    // create an overlay for the image (which is used to play animated gifs)
    // EDIT: well, don't do that here, due to some initialization orders
    //       problem, it might gives an error and not create the overlay
    //       I leave that line here for the records ^^
    //[self setOverlay:[CALayer layer] forType:IKOverlayTypeImage];

    // NOTE: calling this before anything else seems to fix a lot of
    //       problems ... maybe it's initializing a few things internally
    //       on the first call ...
    [super setImageWithURL:nil];
}

-(BOOL)isGIF:(NSString *)path
{
    // checks if the path points to a GIF image
    NSString * pathExtension = [[path pathExtension] lowercaseString];
    return [pathExtension isEqualToString:@"gif"];
}

-(void)setImageWithURL:(NSURL *)url
{
    // EDIT: this is where we create the overlay now, but only if it doesn't
    //       already exists.
    // checks if a layer is already set
    if ([self overlayForType:IKOverlayTypeImage] == nil)
        [self setOverlay:[CALayer layer] forType:IKOverlayTypeImage];

    // remove the overlay animation
    [[self overlayForType:IKOverlayTypeImage] removeAllAnimations];

    // check if it's a gif
    if ([self isGIF:[url path]] == YES)
    {
        // loads the image
        NSImage * image = [[NSImage alloc] initWithContentsOfFile:[url path]];

        // get the image representations, and iterate through them
        NSArray * reps = [image representations];
        for (NSImageRep * rep in reps)
        {
            // find the bitmap representation
            if ([rep isKindOfClass:[NSBitmapImageRep class]] == YES)
            {
                // get the bitmap representation
                NSBitmapImageRep * bitmapRep = (NSBitmapImageRep *)rep;

                // get the number of frames. If it's 0, it's not an animated gif, do nothing
                int numFrame = [[bitmapRep valueForProperty:NSImageFrameCount] intValue];
                if (numFrame == 0)
                    break;

                // create a value array which will contains the frames of the animation
                NSMutableArray * values = [NSMutableArray array];

                // loop through the frames (animationDuration is the duration of the whole animation)
                float animationDuration = 0.0f;
                for (int i = 0; i < numFrame; ++i)
                {
                    // set the current frame
                    [bitmapRep setProperty:NSImageCurrentFrame withValue:[NSNumber numberWithInt:i]];

                    // this part is optional. For some reasons, the NSImage class often loads a GIF with
                    // frame times of 0, so the GIF plays extremely fast. So, we check the frame duration, and if it's
                    // less than a threshold value, we set it to a default value of 1/20 second.
                    if ([[bitmapRep valueForProperty:NSImageCurrentFrameDuration] floatValue] < 0.000001f)
                        [bitmapRep setProperty:NSImageCurrentFrameDuration withValue:[NSNumber numberWithFloat:1.0f / 20.0f]];

                    // add the CGImageRef to this frame to the value array
                    [values addObject:(id)[bitmapRep CGImage]];

                    // update the duration of the animation
                    animationDuration += [[bitmapRep valueForProperty:NSImageCurrentFrameDuration] floatValue];
                }

                // create and setup the animation (this is pretty straightforward)
                CAKeyframeAnimation * animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
                [animation setValues:values];
                [animation setCalculationMode:@"discrete"];
                [animation setDuration:animationDuration];
                [animation setRepeatCount:HUGE_VAL];

                // add the animation to the layer
                [[self overlayForType:IKOverlayTypeImage] addAnimation:animation forKey:@"contents"];

                // stops at the first valid representation
                break;
            }
        }

        // release the image
        [image release];
    }

    // calls the super setImageWithURL method to handle standard images
    [super setImageWithURL:url];
}

@end

So, what I do here is that I create an overlay layer to display the image, and when I find an animated GIF, I load it using the NSImage class, and then I scan it to extract all the frames and setup the layer animation. So to display an animated GIF, you now only have to use the setImageWithURL method of your ImageView class. The advantage of using IKImageView over NSImageView, is that it can be resized while still playing your animation smoothly, and it benefit of all the features of the IKImageView class.

11 thoughts on “Play an animated GIF with an IKImageView

  1. Hi,
    Glad you liked it ^^ Now, how to manually control the animation … that I don’t really know. But after a quick look at the documentation, I noticed that CALayer conforms to the CAMediaTiming protocol. And this protocol provides methods that -I think- could allow you to achieve what you want :

    setSpeed to 0 to pause the animation
    setTimeOffset to control the frame, relative to the current one ?

    Well, the documentation is not always very clear, so you’ll probably have to play with all the properties of CAMediaTiming, but I think it might quite easily let you do what you want ^^

    Regards,
    Damien.

  2. Bonjour Citron,

    I’m having trouble with IKImageView still. I want to create a means to be able to display multi-frame TIFFs in an IKImageView as it has all the convenience methods including zooming, rotating, cropping etc. As we all know, it’s a pain to resend an image to IKImageView as it redraws and doesn’t maintain the zoom / rotation settings from the previous image.

    I have adapted your code for the animations to make it work for TIFFs easily. Your code is great. However what I really want is a means to be able to control the frame with a slider to be able to manually change the frame at will in an IKImageView.

    Do you have any ideas how? I have been thinking about using CALayer sublayers, but I’m completely stuck. Any help you can give would be greatly appreciated. Thanks a lot for your code examples in general.

    Here’s my adapted protocol for the TIFFs just out of interest for you:

    myImageSource = CGImageSourceCreateWithURL((CFURLRef)url, nil);
    int numFrames = (int)CGImageSourceGetCount(myImageSource);

    CAKeyframeAnimation * animation = [CAKeyframeAnimation animationWithKeyPath:@”contents”];
    NSMutableArray * values = [NSMutableArray array];
    imageLayer = [CALayer layer];
    // create the layer, animation and value array
    for(int i = 0; i < numFrames; i++){
    theImage = CGImageSourceCreateImageAtIndex(myImageSource,i,NULL);
    [values addObject:(id)theImage];
    }

    [animation setValues:values];
    [animation setCalculationMode:@"discrete"];
    [animation setDuration:HUGE_VAL];
    [animation setRepeatCount:HUGE_VAL];

    // create the layer and add the animation
    CALayer * layer = [CALayer layer];
    // create the main layer and add the animation
    [layer addAnimation:animation forKey:@"contents"];
    [self setOverlay:layer forType:IKOverlayTypeImage];

  3. Hey ! In case someone has the “could not add […]” problem, i’ve finally fixed it once and for all ! The previous fix did work for a while, and then I got it back after changing a few things in the interface builder …

    Anyway, there 2 things to do to get rid of the bug :

    1) always do a call to [super setImageWithURL:nil] before doing anything else ! It seems that the first time this method is called, the IKImageView will internally initialize a few things, and if you don’t do that, you’ll probably get the error.

    2) When you want to add your custom IKImageView in your application, you’ll probably want to use the interface builder, and drop an “IKImageView” object in your application. DON’T DO IT !! Instead, drop an NSCustomView, and set its class type to your custom image view. You won’t have the IKImageView options showing in the interface bulder, but you also won’t have the bug about CALayer anymore !

    You can check the version greater or equal to v0.5 of the ImageViewer here : http://blog.pcitron.fr/tools/macosx-imageviewer/ to see how it’s done.

  4. I think you may have just cracked exactly what I’ve been striving to do for an age! Thank you so much!!

    1. You’re welcome ^^

      Oh, and by the way, if anyone gets the error reported by Samuel (which I had for a long time) I now have the solution. I updated the sample source code of the article, but in a few words : I was creating the overlay responsible of playing the animation only once in the awakeFromNib method. And for some reason, it was sometime failing (I guess this is something having to do with the objects initialization order)

      So, instead, in the setImageWithUrl method, I check if there is a layer, and if not, I create it. This way, I’m sure all the object have been initialized, and you don’t get the error anymore ^^

  5. Oops, the message got HTML filtered:

    *** could not add ‘[CALayer: 0x102647020]’ linkedTo ‘kIKImageLayerType’

    1. Hi,
      I also have this error, but it appears only once in a while … it seems completely random, and I never found what is the cause. I just close and restart my application to get rid of it.
      If you find the cause of this error, drop a comment here to let me know, I would be interested :)

  6. I found your page when I was trying to do something similar with an overlay layer on an IKImageView. I’m doing essentially the same thing you are in awakeFromNib, but I get the following error in my console:

    *** could not add ” linkedTo ‘kIKImageLayerType’

    Have you seen this before? Any idea how to fix it?

    1. hum, there’s nothing wrong with “for (i = 0; i < numFrame; ++i)" … you should use the contact form, and send me a message (use the "file attachment" to send me the file where you have the error)

Comments are closed.