diff --git a/Demo/demo/classes/ViewController.swift b/Demo/demo/classes/ViewController.swift index b1dfd4e..de286d1 100755 --- a/Demo/demo/classes/ViewController.swift +++ b/Demo/demo/classes/ViewController.swift @@ -8,8 +8,10 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - imageView.setAnimatableImage(named: "mugen.gif") - imageView.startAnimatingGIF() + if let image = AnimatedImage.animatedImageWithName("mugen.gif") { + imageView.setAnimatedImage(image) + imageView.startAnimatingGIF() + } UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: false) } diff --git a/Gifu.xcodeproj/project.pbxproj b/Gifu.xcodeproj/project.pbxproj index c481657..e698be0 100644 --- a/Gifu.xcodeproj/project.pbxproj +++ b/Gifu.xcodeproj/project.pbxproj @@ -7,19 +7,25 @@ objects = { /* Begin PBXBuildFile section */ - 00B8C75E1A364DCE00C188E7 /* Gifu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B8C75B1A364DCE00C188E7 /* Gifu.swift */; }; + 00B8C75E1A364DCE00C188E7 /* AnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B8C75B1A364DCE00C188E7 /* AnimatedImage.swift */; }; 00B8C75F1A364DCE00C188E7 /* ImageSourceHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B8C75C1A364DCE00C188E7 /* ImageSourceHelpers.swift */; }; 00B8C7601A364DCE00C188E7 /* UIImageView+Gifu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B8C75D1A364DCE00C188E7 /* UIImageView+Gifu.swift */; }; 00B8C7961A3650EE00C188E7 /* Gifu.h in Headers */ = {isa = PBXBuildFile; fileRef = 00B8C7951A3650EE00C188E7 /* Gifu.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EAF49C7F1A3A4DE000B395DF /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF49C7E1A3A4DE000B395DF /* UIImageExtension.swift */; }; + EAF49C811A3A4FAA00B395DF /* Functional.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF49C801A3A4FAA00B395DF /* Functional.swift */; }; + EAF49CB11A3B6EEB00B395DF /* AnimatedFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF49CB01A3B6EEB00B395DF /* AnimatedFrame.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 00B8C73E1A364DA400C188E7 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 00B8C7421A364DA400C188E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Source/Info.plist; sourceTree = ""; }; - 00B8C75B1A364DCE00C188E7 /* Gifu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Gifu.swift; sourceTree = ""; }; + 00B8C75B1A364DCE00C188E7 /* AnimatedImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImage.swift; sourceTree = ""; }; 00B8C75C1A364DCE00C188E7 /* ImageSourceHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSourceHelpers.swift; sourceTree = ""; }; 00B8C75D1A364DCE00C188E7 /* UIImageView+Gifu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+Gifu.swift"; sourceTree = ""; }; 00B8C7951A3650EE00C188E7 /* Gifu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Gifu.h; sourceTree = ""; }; + EAF49C7E1A3A4DE000B395DF /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; + EAF49C801A3A4FAA00B395DF /* Functional.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functional.swift; sourceTree = ""; }; + EAF49CB01A3B6EEB00B395DF /* AnimatedFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedFrame.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -63,9 +69,12 @@ isa = PBXGroup; children = ( 00B8C7951A3650EE00C188E7 /* Gifu.h */, - 00B8C75B1A364DCE00C188E7 /* Gifu.swift */, + 00B8C75B1A364DCE00C188E7 /* AnimatedImage.swift */, 00B8C75C1A364DCE00C188E7 /* ImageSourceHelpers.swift */, 00B8C75D1A364DCE00C188E7 /* UIImageView+Gifu.swift */, + EAF49C7E1A3A4DE000B395DF /* UIImageExtension.swift */, + EAF49C801A3A4FAA00B395DF /* Functional.swift */, + EAF49CB01A3B6EEB00B395DF /* AnimatedFrame.swift */, ); path = Source; sourceTree = ""; @@ -148,9 +157,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + EAF49CB11A3B6EEB00B395DF /* AnimatedFrame.swift in Sources */, 00B8C7601A364DCE00C188E7 /* UIImageView+Gifu.swift in Sources */, 00B8C75F1A364DCE00C188E7 /* ImageSourceHelpers.swift in Sources */, - 00B8C75E1A364DCE00C188E7 /* Gifu.swift in Sources */, + EAF49C811A3A4FAA00B395DF /* Functional.swift in Sources */, + 00B8C75E1A364DCE00C188E7 /* AnimatedImage.swift in Sources */, + EAF49C7F1A3A4DE000B395DF /* UIImageExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Source/AnimatedFrame.swift b/Source/AnimatedFrame.swift new file mode 100644 index 0000000..bb0cf2f --- /dev/null +++ b/Source/AnimatedFrame.swift @@ -0,0 +1,4 @@ +struct AnimatedFrame { + let image: UIImage? + let duration: NSTimeInterval +} diff --git a/Source/AnimatedImage.swift b/Source/AnimatedImage.swift new file mode 100755 index 0000000..a2b9b73 --- /dev/null +++ b/Source/AnimatedImage.swift @@ -0,0 +1,116 @@ +import UIKit +import ImageIO + +public class AnimatedImage: UIImage { + // MARK: - Constants + let maxTimeStep = 1.0 + + // MARK: - Public Properties + var delegate: UIImageView? + var animatedFrames = [AnimatedFrame]() + var totalDuration: NSTimeInterval = 0.0 + + // MARK: - Private Properties + private lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: "updateCurrentFrame") + private var currentFrameIndex = 0 + private var timeSinceLastFrameChange: NSTimeInterval = 0.0 + + // MARK: - Computed Properties + var currentFrame: UIImage? { + return frameAtIndex(currentFrameIndex) + } + + private var isAnimated: Bool { + return totalDuration != 0.0 + } + + // MARK: - Initializers + required public init(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + public override convenience init(data: NSData) { + self.init(data: data, size: CGSizeZero) + } + + required public init(data: NSData, size: CGSize) { + super.init() + + let imageSource = CGImageSourceCreateWithData(data, nil) + attachDisplayLink() + curry(prepareFrames) <^> imageSource <*> size + pauseAnimation() + } + + // MARK: - Factories + public class func animatedImageWithName(name: String) -> AnimatedImage? { + let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(name) + return animatedImageWithData <^> NSData(contentsOfFile: path) + } + + public class func animatedImageWithData(data: NSData) -> AnimatedImage { + let size = UIImage.sizeForImageData(data) ?? CGSizeZero + return self(data: data, size: size) + } + + public class func animatedImageWithName(name: String, size: CGSize) -> AnimatedImage? { + let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(name) + return curry(animatedImageWithData) <^> NSData(contentsOfFile: path) <*> size + } + + public class func animatedImageWithData(data: NSData, size: CGSize) -> AnimatedImage { + return self(data: data, size: size) + } + + // MARK: - Display Link Helpers + func attachDisplayLink() { + displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes) + } + + // MARK: - Frame Methods + private func prepareFrames(imageSource: CGImageSourceRef, size: CGSize) { + let numberOfFrames = Int(CGImageSourceGetCount(imageSource)) + animatedFrames.reserveCapacity(numberOfFrames) + + (animatedFrames, totalDuration) = reduce(0.. UIImage? { + if index >= animatedFrames.count { return .None } + return animatedFrames[index].image + } + + func updateCurrentFrame() { + if !isAnimated { return } + + timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration) + var frameDuration = animatedFrames[currentFrameIndex].duration + + if timeSinceLastFrameChange >= frameDuration { + timeSinceLastFrameChange -= frameDuration + currentFrameIndex = ++currentFrameIndex % animatedFrames.count + delegate?.layer.setNeedsDisplay() + } + } + + // MARK: - Animation + func pauseAnimation() { + displayLink.paused = true + } + + func resumeAnimation() { + if isAnimated { + displayLink.paused = false + } + } + + func isAnimating() -> Bool { + return !displayLink.paused + } +} diff --git a/Source/Functional.swift b/Source/Functional.swift new file mode 100644 index 0000000..10a420d --- /dev/null +++ b/Source/Functional.swift @@ -0,0 +1,28 @@ +infix operator >>- { associativity left precedence 150 } +infix operator <^> { associativity left precedence 150 } +infix operator <*> { associativity left precedence 150 } + +func >>-(a: A?, f: A -> B?) -> B? { + switch a { + case let .Some(x): return f(x) + case .None: return .None + } +} + +func <^>(f: A -> B, a: A?) -> B? { + switch a { + case let .Some(x): return f(x) + case .None: return .None + } +} + +func <*>(f: (A -> B)?, a: A?) -> B? { + switch f { + case let .Some(fx): return fx <^> a + case .None: return .None + } +} + +func curry(f: (A, B) -> C) -> A -> B -> C { + return { a in { b in f(a, b) } } +} diff --git a/Source/Gifu.swift b/Source/Gifu.swift deleted file mode 100755 index 4c76ba1..0000000 --- a/Source/Gifu.swift +++ /dev/null @@ -1,151 +0,0 @@ -import UIKit -import ImageIO - -public 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_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 public init(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - required public init(data: NSData, delegate: UIImageView?) { - let imageSource = CGImageSourceCreateWithData(data, nil) - self.delegate = delegate - - if CGImageSourceContainsAnimatedGIF(imageSource) { - super.init() - attachDisplayLink() - prepareFrames(imageSource) - pauseAnimation() - } else { - super.init(data: data) - } - } - - // MARK: - Factories - class func imageWithName(name: String, delegate: UIImageView?) -> Self? { - let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(name) - - if let data = NSData(contentsOfFile: path) { - return imageWithData(data, delegate: delegate) - } - - return 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 = Int(CGImageSourceGetCount(self.imageSource)) - frameDurations.reserveCapacity(numberOfFrames) - frames.reserveCapacity(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 pauseAnimation() { - displayLink.paused = true - } - - func resumeAnimation() { - displayLink.paused = false - } - - func isAnimating() -> Bool { - return !displayLink.paused - } -} diff --git a/Source/ImageSourceHelpers.swift b/Source/ImageSourceHelpers.swift index 32ca990..e812a72 100755 --- a/Source/ImageSourceHelpers.swift +++ b/Source/ImageSourceHelpers.swift @@ -2,7 +2,7 @@ import UIKit import ImageIO import MobileCoreServices -func CGImageSourceContainsAnimatedGIF(imageSource: CGImageSource) -> Bool { +private func CGImageSourceContainsAnimatedGIF(imageSource: CGImageSource) -> Bool { let isTypeGIF = UTTypeConformsTo(CGImageSourceGetType(imageSource), kUTTypeGIF) let imageCount = CGImageSourceGetCount(imageSource) return isTypeGIF != 0 && imageCount > 1 diff --git a/Source/UIImageExtension.swift b/Source/UIImageExtension.swift new file mode 100644 index 0000000..3ba0956 --- /dev/null +++ b/Source/UIImageExtension.swift @@ -0,0 +1,16 @@ +extension UIImage { + func resize(size: CGSize) -> UIImage { + UIGraphicsBeginImageContext(size) + self.drawInRect(CGRectMake(0, 0, size.width, size.height)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return newImage + } + + class func imageWithData(data: NSData, size: CGSize) -> UIImage? { + return UIImage(data: data)?.resize(size) + } + class func sizeForImageData(data: NSData) -> CGSize? { + return UIImage(data: data)?.size + } +} diff --git a/Source/UIImageView+Gifu.swift b/Source/UIImageView+Gifu.swift index 6db3ccb..4463525 100755 --- a/Source/UIImageView+Gifu.swift +++ b/Source/UIImageView+Gifu.swift @@ -3,11 +3,7 @@ import UIKit public extension UIImageView { // MARK: - Computed Properties var animatableImage: AnimatedImage? { - if image is AnimatedImage { - return image as? AnimatedImage - } else { - return nil - } + return image as? AnimatedImage } var isAnimatingGIF: Bool { @@ -15,43 +11,27 @@ public extension UIImageView { } var animatable: Bool { - return animatableImage != nil + return animatableImage != .None } // MARK: - Method Overrides override public func displayLayer(layer: CALayer!) { - if let image = animatableImage { - if let frame = image.currentFrame { - layer.contents = frame.CGImage - } - } + layer.contents = animatableImage?.currentFrame?.CGImage } // MARK: - Setter Methods - func setAnimatableImage(named name: String) { - image = AnimatedImage.imageWithName(name, delegate: self) - layer.setNeedsDisplay() - } - - func setAnimatableImage(#data: NSData) { - image = AnimatedImage.imageWithData(data, delegate: self) + public func setAnimatedImage(image: AnimatedImage) { + image.delegate = self + self.image = image layer.setNeedsDisplay() } // MARK: - Animation func startAnimatingGIF() { - if animatable { - animatableImage!.resumeAnimation() - } else { - startAnimating() - } + animatableImage?.resumeAnimation() ?? startAnimating() } func stopAnimatingGIF() { - if animatable { - animatableImage!.pauseAnimation() - } else { - stopAnimating() - } + animatableImage?.pauseAnimation() ?? stopAnimating() } }