commit 25221506bd280a018659c7931e42acccf742ab59 Author: Reda Lemeden Date: Sat Sep 6 16:12:09 2014 +0200 Initial commit diff --git a/ImageSourceHelpers.swift b/ImageSourceHelpers.swift new file mode 100644 index 0000000..3311a27 --- /dev/null +++ b/ImageSourceHelpers.swift @@ -0,0 +1,34 @@ +import UIKit +import ImageIO +import MobileCoreServices + +func CGImageSourceContainsAnimatedGIF(imageSource: CGImageSource) -> Bool { + let isTypeGIF = UTTypeConformsTo(CGImageSourceGetType(imageSource), kUTTypeGIF) + let imageCount = CGImageSourceGetCount(imageSource) + return isTypeGIF != 0 && imageCount > 1 +} + +func CGImageSourceGIFFrameDuration(imageSource: CGImageSource, index: Int) -> NSTimeInterval { + let containsAnimatedGIF = CGImageSourceContainsAnimatedGIF(imageSource) + if !containsAnimatedGIF { return 0.0 } + + var duration = 0.0 + let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, UInt(index), nil) as NSDictionary + let GIFProperties: NSDictionary? = imageProperties[kCGImagePropertyGIFDictionary] as? NSDictionary + + if let properties = GIFProperties { + duration = properties[kCGImagePropertyGIFUnclampedDelayTime] as Double + + if duration <= 0 { + duration = properties[kCGImagePropertyGIFDelayTime] as Double + } + } + + let threshold = 0.02 - Double(FLT_EPSILON) + + if duration > 0 && duration < threshold { + duration = 0.1 + } + + return duration +} diff --git a/Matsuri.swift b/Matsuri.swift new file mode 100644 index 0000000..0fd2dae --- /dev/null +++ b/Matsuri.swift @@ -0,0 +1,149 @@ +import UIKit +import ImageIO + +class AnimatedImage: UIImage { + // MARK: - Constants + let framesToPreload = 10 + let maxTimeStep = 1.0 + + // MARK: - Public Properties + var delegate: UIImageView? + var frameDurations = [NSTimeInterval]() + var frames = [UIImage?]() + var totalDuration: NSTimeInterval = 0.0 + + // MARK: - Private Properties + private lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: "updateCurrentFrame") + private lazy var preloadFrameQueue: dispatch_queue_t = dispatch_queue_create("co.kaishin.GIFPreloadImages", DISPATCH_QUEUE_SERIAL) + private var currentFrameIndex = 0 + private var imageSource: CGImageSource? + private var timeSinceLastFrameChange: NSTimeInterval = 0.0 + + // MARK: - Computed Properties + var currentFrame: UIImage? { + return frameAtIndex(currentFrameIndex) + } + + private var isAnimated: Bool { + return imageSource != nil + } + + // MARK: - Initializers + required init(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + required init(data: NSData, delegate: UIImageView?) { + let imageSource = CGImageSourceCreateWithData(data, nil) + self.delegate = delegate + + if CGImageSourceContainsAnimatedGIF(imageSource) { + super.init() + attachDisplayLink() + prepareFrames(imageSource) + startAnimating() + } else { + super.init(data: data) + stopAnimating() + } + } + + // MARK: - Factories + class func imageWithName(name: String, delegate: UIImageView?) -> Self? { + let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(name) + let data = NSData.dataWithContentsOfFile(path, options: nil, error: nil) + return (data != nil) ? imageWithData(data, delegate: delegate) : nil + } + + class func imageWithData(data: NSData, delegate: UIImageView?) -> Self? { + return self(data: data, delegate: delegate) + } + + // MARK: - Display Link Helpers + func attachDisplayLink() { + displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes) + } + + // MARK: - Frame Methods + private func prepareFrames(source: CGImageSource!) { + imageSource = source + + let numberOfFrames = CGImageSourceGetCount(self.imageSource) + frameDurations.reserveCapacity(Int(numberOfFrames)) + frames.reserveCapacity(Int(numberOfFrames)) + + for index in 0.. UIImage? { + if Int(index) >= self.frames.count { return nil } + + var image: UIImage? = self.frames[Int(index)] + + updatePreloadedFramesAtIndex(index) + + return image + } + + private func updatePreloadedFramesAtIndex(index: Int) { + if frames.count <= framesToPreload { return } + + if index != 0 { + frames[index] = nil + } + + for internalIndex in (index + 1)...(index + framesToPreload) { + let adjustedIndex = internalIndex % frames.count + + if frames[adjustedIndex] == nil { + dispatch_async(preloadFrameQueue) { + let frameImageRef = CGImageSourceCreateImageAtIndex(self.imageSource, UInt(adjustedIndex), nil) + self.frames[adjustedIndex] = UIImage(CGImage: frameImageRef) + } + } + } + } + + func updateCurrentFrame() { + if !isAnimated { return } + + timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration) + var frameDuration = frameDurations[currentFrameIndex] + + while timeSinceLastFrameChange >= frameDuration { + timeSinceLastFrameChange -= frameDuration + currentFrameIndex++ + + if currentFrameIndex >= frames.count { + currentFrameIndex = 0 + } + + delegate?.layer.setNeedsDisplay() + } + } + + // MARK: - Animation + func stopAnimating() { + displayLink.paused = true + } + + func startAnimating() { + displayLink.paused = false + } + + func isAnimating() -> Bool { + return !displayLink.paused + } +} \ No newline at end of file diff --git a/UIImageView+Matsuri.swift b/UIImageView+Matsuri.swift new file mode 100644 index 0000000..9b49a6a --- /dev/null +++ b/UIImageView+Matsuri.swift @@ -0,0 +1,51 @@ +import UIKit + +extension UIImageView { + // MARK: - Computed Properties + var animatableImage: AnimatedImage? { + if image is AnimatedImage { + return image as? AnimatedImage + } else { + return nil + } + } + + var isAnimating: Bool { + return animatableImage?.isAnimating() ?? false + } + + var animatable: Bool { + return animatableImage != nil + } + + // MARK: - Method Overrides + override public func displayLayer(layer: CALayer!) { + if let image = animatableImage { + if let frame = image.currentFrame { + layer.contents = frame.CGImage + } + } + } + + // MARK: - Setter Functions + func setAnimatedImage(named name: String) { + image = AnimatedImage.imageWithName(name, delegate: self) + } + + func setAnimatedImage(#data: NSData) { + image = AnimatedImage.imageWithData(data, delegate: self) + } + + // MARK: - Animation + func startAnimating() { + if animatable { + animatableImage!.startAnimating() + } + } + + func stopAnimating() { + if animatable { + animatableImage!.stopAnimating() + } + } +}