74

I have a series of user-customized images within an iOS app that are being animated in a simple, frame-by-frame flip book style.

My question is this: is there a way to allow users to export their animation as an animated gif? Ideally, I'd like to enable them to email, social share (T/FB) or (worst case..) save an animated gif to their documents folder for retrieval via iTunes.

I know how to save a .png to the photo library, and I found a way to record an animation as a QT file (http://www.cimgf.com/2009/02/03/record-your-core-animation-animation/), but I haven't found a way to just kick out a plain old animated gif. Am I missing something in Core Animation or somewhere else? Are there any approaches, frameworks, or resources that anyone can recommend? Sorry if the question is too general - struggling to find a starting point.

Cœur
  • 37,241
  • 25
  • 195
  • 267
crgt
  • 1,482
  • 1
  • 14
  • 17

3 Answers3

160

You can create an animated GIF using the Image I/O framework (which is part of the iOS SDK). You will also want to include the MobileCoreServices framework, which defines the GIF type constant. You need to add these frameworks to your target, and import their headers in the file where you want to create the animated GIF, like this:

#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/MobileCoreServices.h>

It's easiest to explain by example. I'll show you the code I used to make this GIF on my iPhone 5:

animated GIF created by the code shown

First, here's a helper function that takes a size and an angle and returns a UIImage of the red disk at that angle:

static UIImage *frameImage(CGSize size, CGFloat radians) {
    UIGraphicsBeginImageContextWithOptions(size, YES, 1); {
        [[UIColor whiteColor] setFill];
        UIRectFill(CGRectInfinite);
        CGContextRef gc = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(gc, size.width / 2, size.height / 2);
        CGContextRotateCTM(gc, radians);
        CGContextTranslateCTM(gc, size.width / 4, 0);
        [[UIColor redColor] setFill];
        CGFloat w = size.width / 10;
        CGContextFillEllipseInRect(gc, CGRectMake(-w / 2, -w / 2, w, w));
    }
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

Now we can create the GIF. First we'll define a constant for the number of frames, because we need it twice later:

static void makeAnimatedGif(void) {
    static NSUInteger const kFrameCount = 16;

We'll need a property dictionary to specify the number of times the animation should repeat:

    NSDictionary *fileProperties = @{
        (__bridge id)kCGImagePropertyGIFDictionary: @{
            (__bridge id)kCGImagePropertyGIFLoopCount: @0, // 0 means loop forever
        }
    };

And we'll need another property dictionary, which we'll attach to each frame, specifying how long that frame should be displayed:

    NSDictionary *frameProperties = @{
        (__bridge id)kCGImagePropertyGIFDictionary: @{
            (__bridge id)kCGImagePropertyGIFDelayTime: @0.02f, // a float (not double!) in seconds, rounded to centiseconds in the GIF data
        }
    };

We'll also create a URL for the GIF in our documents directory:

    NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
    NSURL *fileURL = [documentsDirectoryURL URLByAppendingPathComponent:@"animated.gif"];

Now we can create a CGImageDestination that writes a GIF to the specified URL:

    CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)fileURL, kUTTypeGIF, kFrameCount, NULL);
    CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)fileProperties);

I discovered that passing fileProperties as the last argument of CGImageDestinationCreateWithURL does not work. You have to use CGImageDestinationSetProperties.

Now we can create and write our frames:

    for (NSUInteger i = 0; i < kFrameCount; i++) {
        @autoreleasepool {
            UIImage *image = frameImage(CGSizeMake(300, 300), M_PI * 2 * i / kFrameCount);
            CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
        }
    }

Note that we pass the frame properties dictionary along with each frame image.

After we've added exactly the specified number of frames, we finalize the destination and release it:

    if (!CGImageDestinationFinalize(destination)) {
        NSLog(@"failed to finalize image destination");
    }
    CFRelease(destination);

    NSLog(@"url=%@", fileURL);
}

If you run this on the simulator, you can copy the URL from the debug console and paste it into your browser to see the image. If you run it on the device, you can use the Xcode Organizer window to download the app sandbox from the device and look at the image. Or you can use an app like iExplorer that lets you browse your device's filesystem directly. (This doesn't require jailbreaking.)

I tested this on my iPhone 5 running iOS 6.1, but I believe the code should work as far back as iOS 4.0.

I've put all the code in this gist for easy copying.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • This is something I've been looking for for a while. (Creating animated GIFs). The actual generation of the image is fairly trivial, and easy enough to replace with code from my application. The tricky bit is creating the actual GIF image, and you've given me everything I need to do that. Thank you! (Voted) – Duncan C Feb 18 '13 at 13:57
  • Agree with Duncan - thanks Rob! Still have to give this a whirl tonight, but it's everything I was looking for...thanks again!! – crgt Feb 19 '13 at 02:21
  • 1
    Rob, I got your technique working today, but there is a big limitation. The system retains the data for all of the images in the animation sequence in memory until the finalize call is complete. I'm creating some pretty big GIF sequences, and at 30 FPS it doesn't take long to run out of memory and crash. Is there any way to buffer the frames to disk somehow? I am creating my CGImages from OpenGL, and using a data provider. There is a form of data provider that reads it's contents from a file. I wonder if that would read each frame in turn and release it as it finalized the GIF. – Duncan C Feb 19 '13 at 02:45
  • You'll have to test whether `CGImageDestinationAddImageFromSource` yourself. I haven't played with it. If that doesn't help you, you'll have to look for or implement an animated GIF encoder yourself. – rob mayoff Feb 19 '13 at 03:18
  • 3
    A very quick search and inspection leads me to think [Giraffe](https://github.com/unixpickle/Giraffe) might do what you want. – rob mayoff Feb 19 '13 at 03:21
  • I tried changing the code above to load images from a file-based image provider using CGDataProviderCreateWithFilename, and it didn't make any difference in the memory footprint. The app still runs out of memory and crashes if you try to write too many frames. – Duncan C Feb 22 '13 at 01:30
  • Works fine for me ! the only thing I can't manage to do is how to change the gif delay time from a user generated input ? what does "@" means in : (__bridge id)kCGImagePropertyGIFDelayTime: @0.02f – Diwann Jun 11 '13 at 21:35
  • `@0.02f` means `[NSNumber numberWithFloat:0.02f]`. See the [documentation](http://clang.llvm.org/docs/ObjectiveCLiterals.html#nsnumber-literals). – rob mayoff Jun 11 '13 at 22:13
  • but why does it fail if I replace @0.02f with "[NSNumber numberWithFloat:0.02f]" ? (the error is : CGDataConsumer(url_close): write failed: -15. at the line : CFRelease(destination) ) – Diwann Jun 11 '13 at 23:10
  • arg. I found it. It wasn't related... it was because i had also modified the path to : [NSURL URLWithString:[self getNewPath]], and I think that CGImageDestinationCreateWithURL needs a full NSURL generated url and not an url generated from a NSString (to position NSUserDomainMask perhaps ?). Anyway, thanks for your help, it's working like a charm now ! – Diwann Jun 12 '13 at 00:18
  • I've been trying to look around but can't seem to find any info. I have an app that lets users create/import/share animated gifs through messaging but was curious if it's possible to share gifs with facebook and twitter. I've used ShareKit before on other apps but only to share JPG/PNG files. – rob1302 Sep 03 '13 at 05:45
  • 1
    @robmayoff Hi Rob, I've been using your code for quite some time, but I found a strange bug, I always get EXE_BAD_ACCESS at CGImageDestinationFinalize(destination) if there are identical frames next to each other in input sequence. Any idea why this is happening? – X.Y. Oct 16 '13 at 05:49
  • I don't work on iOS stuff anymore. You can try asking a new top-level question. If you include some code to reproduce the problem, you might get some help. – rob mayoff Oct 16 '13 at 06:09
  • @phantomlimb i have the exact same problem. Did you find a solution for this? – Pascal Nov 21 '13 at 07:33
  • 1
    @Pascal It's a system bug, you cannot have identical frames at the end of your sequence, i.e. if you have n frames, n-th and n-1th frame cannot be the same. I ended up trimming the tail of my sequence for identical frames. (And I use a very naive way to detect identical frames, file size plus the time stamp in EXIF tag, worked very well for me...) – X.Y. Nov 21 '13 at 09:32
  • @phantomlimb thank you phantom....this drove me nuts. I used the same image for all the frames for testing purposes...argh!!! – Pascal Nov 21 '13 at 16:22
  • Could somebody point me in the right direction to then take the file we just created and save it to the camera roll? Is that possible? – flynn Mar 21 '14 at 19:06
  • 1
    @kevin Check [this question](http://stackoverflow.com/q/15079262/77567) and its comments. I haven't tested, but I have no reason to doubt that information. – rob mayoff Mar 22 '14 at 07:08
  • @robmayoff Is there a way to generate true color gifs on iOS? See this question http://stackoverflow.com/questions/24535149/how-do-i-generat-true-color-animated-gifs-in-ios – Michael Jul 03 '14 at 05:43
  • The output GIF file from this method is a GIF-87a format, which is old format of the GIF. GIF-89a is the newer format which supports animation and multiple files. As the GIFs from this method support animation and multiple files, why does it ave a GIF-87a HEX signature? – Haris Hussain Jul 28 '14 at 12:51
  • For anyone using `+dataWithContentsOfURL:` to retrieve the image later, I had to call `CGImageDestinationFinalize(destination);` before releasing the image for it to work properly. Great tutorial though, thank you! – rebello95 Apr 30 '15 at 04:00
  • @DuncanC Did you ever find a solution for this? I'm operating under a severely memory-constrained environment, and I suspect I'll run into this. Any luck? I had thought about creating an `mmap`-backed file on disk to create the bitmap context with, but I'm not sure yet... – CIFilter Jul 14 '15 at 04:05
  • Hello, I am using you code. It's great thank you. But when i try to create a more bigger gif the app it's crashing. I spoke by a gif with 100 frames. It's any way to make it works well? – Gaby Fitcal Aug 21 '15 at 11:35
  • @robmayoff i implement your code but always call NSLog(@"failed to finalize image destination"); line. Any solution?? – Pramod Tapaniya Jun 28 '16 at 05:30
2

For Swift 3

import Foundation
import UIKit
import ImageIO
import MobileCoreServices

extension UIImage {
    static func animatedGif(from images: [UIImage]) {
        let fileProperties: CFDictionary = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]]  as CFDictionary
        let frameProperties: CFDictionary = [kCGImagePropertyGIFDictionary as String: [(kCGImagePropertyGIFDelayTime as String): 1.0]] as CFDictionary

        let documentsDirectoryURL: URL? = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        let fileURL: URL? = documentsDirectoryURL?.appendingPathComponent("animated.gif")

        if let url = fileURL as CFURL? {
            if let destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, images.count, nil) {
                CGImageDestinationSetProperties(destination, fileProperties)
                for image in images {
                    if let cgImage = image.cgImage {
                        CGImageDestinationAddImage(destination, cgImage, frameProperties)
                    }
                }
                if !CGImageDestinationFinalize(destination) {
                    print("Failed to finalize the image destination")
                }
                print("Url = \(fileURL)")
            }
        }
    }
}

I have converted it from the above answer. I hope it helps.

Available as a gist.

Edits are welcomed.

Nikhil Manapure
  • 3,748
  • 2
  • 30
  • 55
0

If you're looking for Swift 3 solution, you can take a look at https://github.com/onmyway133/GifMagic. It has Encoder and Decoder which assembles and disassembles gif file.

Basically, you should use Image IO framework with these functions CGImageDestinationCreateWithURL, CGImageDestinationSetProperties, CGImageDestinationAddImage, CGImageDestinationFinalize

Also with Swift https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html

Core Foundation objects returned from annotated APIs are automatically memory managed in Swift—you do not need to invoke the CFRetain, CFRelease, or CFAutorelease functions yourself.

onmyway133
  • 45,645
  • 31
  • 257
  • 263