From 4ada52c333f40636ad1f3fa97cf94cbfea5f1428 Mon Sep 17 00:00:00 2001 From: Reda Lemeden Date: Fri, 23 Jan 2015 01:02:08 +0100 Subject: [PATCH] Add inline docs --- Source/Animatable.swift | 2 ++ Source/AnimatableImageView.swift | 21 +++++++++++++++++- Source/AnimatedFrame.swift | 1 + Source/Animator.swift | 38 +++++++++++++++++++++++++++----- Source/Curry.swift | 1 + Source/ImageSourceHelpers.swift | 27 +++++++++++++++++++++-- Source/UIImageExtension.swift | 15 +++++++++++++ 7 files changed, 97 insertions(+), 8 deletions(-) diff --git a/Source/Animatable.swift b/Source/Animatable.swift index 2310183..1df1e13 100644 --- a/Source/Animatable.swift +++ b/Source/Animatable.swift @@ -1,3 +1,5 @@ +/// Protocol that requires its members to have a `layer` and a `frame` property. +/// Classes confirming to this protocol can serve as a delegate to `Animator`. protocol Animatable { var layer: CALayer { get } var frame: CGRect { get } diff --git a/Source/AnimatableImageView.swift b/Source/AnimatableImageView.swift index d95b3c6..ce886d6 100644 --- a/Source/AnimatableImageView.swift +++ b/Source/AnimatableImageView.swift @@ -1,42 +1,61 @@ -import UIKit import ImageIO import Runes +import UIKit +/// A subclass of `UIImageView` that can be animated using an image name string or raw data. public class AnimatableImageView: UIImageView, Animatable { + /// An `Animator` instance that holds the frames of a specific image in memory. var animator: Animator? + /// A computed property that returns whether the image view is animating. public var isAnimatingGIF: Bool { return animator?.isAnimating ?? isAnimating() } + /// Prepares the frames using a GIF image file name, without starting the animation. + /// The file name should include the `.gif` extension. + /// + /// :param: imageName The name of the GIF file. The method looks for the file in the app bundle. public func prepareForAnimation(imageNamed imageName: String) { let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(imageName) prepareForAnimation <^> NSData(contentsOfFile: path) } + /// Prepares the frames using raw GIF image data, without starting the animation. + /// + /// :param: data GIF image data. public func prepareForAnimation(imageData data: NSData) { image = UIImage(data: data) animator = Animator(data: data, delegate: self) } + /// Prepares the frames using a GIF image file name and starts animating the image view. + /// + /// :param: imageName The name of the GIF file. The method looks for the file in the app bundle. public func animateWithImage(named imageName: String) { prepareForAnimation(imageNamed: imageName) startAnimatingGIF() } + /// Prepares the frames using raw GIF image data and starts animating the image view. + /// + /// :param: data GIF image data. public func animateWithImageData(#data: NSData) { prepareForAnimation(imageData: data) startAnimatingGIF() } + /// Updates the `UIImage` property of the image view if necessary. This method should not be called manually. override public func displayLayer(layer: CALayer!) { image = animator?.currentFrame? } + /// Starts the image view animation. public func startAnimatingGIF() { animator?.resumeAnimation() ?? startAnimating() } + /// Stops the image view animation. public func stopAnimatingGIF() { animator?.pauseAnimation() ?? stopAnimating() } diff --git a/Source/AnimatedFrame.swift b/Source/AnimatedFrame.swift index bb0cf2f..f0552c8 100644 --- a/Source/AnimatedFrame.swift +++ b/Source/AnimatedFrame.swift @@ -1,3 +1,4 @@ +/// Keeps a reference to an `UIImage` instance and its duration as a GIF frame. struct AnimatedFrame { let image: UIImage? let duration: NSTimeInterval diff --git a/Source/Animator.swift b/Source/Animator.swift index f11954b..25e19e2 100644 --- a/Source/Animator.swift +++ b/Source/Animator.swift @@ -2,23 +2,37 @@ import UIKit import ImageIO import Runes +/// Responsible for storing and updating the frames of a `AnimatableImageView` instance via delegation. class Animator: NSObject { - let maxTimeStep = 1.0 - var animatedFrames = [AnimatedFrame]() - var totalDuration: NSTimeInterval = 0.0 + /// The animator delegate. Should conform to the `Animatable` protocol. let delegate: Animatable + /// Maximum duration to increment the frame timer with. + private let maxTimeStep = 1.0 + /// The total duration of the GIF image. + private var totalDuration: NSTimeInterval = 0.0 + /// An array of animated frames from a single GIF image. + private var animatedFrames = [AnimatedFrame]() + /// The index of the current GIF frame. private var currentFrameIndex = 0 + /// Time elapsed since the last frame change. Used to determine when the frame should be updated. private var timeSinceLastFrameChange: NSTimeInterval = 0.0 + /// A display link that keeps calling the `updateCurrentFrame` method on every screen refresh. private lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: "updateCurrentFrame") + /// The current image frame to show. var currentFrame: UIImage? { return frameAtIndex(currentFrameIndex) } + /// Returns whether the animator is animating. var isAnimating: Bool { return !displayLink.paused } + /// Initializes an animator instance from raw GIF image data and an `Animatable` delegate. + /// + /// :param: data The raw GIF image data. + /// :param: delegate An `Animatable` delegate. required init(data: NSData, delegate: Animatable) { let imageSource = CGImageSourceCreateWithData(data, nil) self.delegate = delegate @@ -29,6 +43,10 @@ class Animator: NSObject { } // MARK: - Frames + /// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`. + /// + /// :param: imageSource The `CGImageSourceRef` image source to extract the frames from. + /// :param: size The size to use for the cached frames. private func prepareFrames(imageSource: CGImageSourceRef, size: CGSize) { let numberOfFrames = Int(CGImageSourceGetCount(imageSource)) animatedFrames.reserveCapacity(numberOfFrames) @@ -46,12 +64,19 @@ class Animator: NSObject { } } - func frameAtIndex(index: Int) -> UIImage? { + /// Returns the frame at a particular index. + /// + /// :param: index The index of the frame. + /// :returns: An optional image at a given frame. + private func frameAtIndex(index: Int) -> UIImage? { if index >= animatedFrames.count { return .None } return animatedFrames[index].image } - func updateCurrentFrame() { + /// Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`. + /// + /// :returns: An optional image at a given frame. + private func updateCurrentFrame() { if totalDuration == 0 { return } timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration) @@ -65,16 +90,19 @@ class Animator: NSObject { } // MARK: - Animation + /// Pauses the display link. func pauseAnimation() { displayLink.paused = true } + /// Resumes the display link. func resumeAnimation() { if totalDuration > 0 { displayLink.paused = false } } + /// Attaches the dsiplay link. func attachDisplayLink() { displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes) } diff --git a/Source/Curry.swift b/Source/Curry.swift index b51c761..7bbba8b 100644 --- a/Source/Curry.swift +++ b/Source/Curry.swift @@ -1,3 +1,4 @@ +/// One of my favorite indian spices. func curry(f: (A, B) -> C) -> A -> B -> C { return { a in { b in f(a, b) } } } diff --git a/Source/ImageSourceHelpers.swift b/Source/ImageSourceHelpers.swift index 38823ee..a0d71c4 100755 --- a/Source/ImageSourceHelpers.swift +++ b/Source/ImageSourceHelpers.swift @@ -1,11 +1,14 @@ -import UIKit import ImageIO import MobileCoreServices import Runes +import UIKit -internal typealias GIFProperties = [String : Double] +typealias GIFProperties = [String : Double] private let defaultDuration: Double = 0 +/// Retruns the duration of a frame at a specific index using an image source (an `CGImageSource` instance). +/// +/// :returns: A frame duration. func CGImageSourceGIFFrameDuration(imageSource: CGImageSource, index: Int) -> NSTimeInterval { if !imageSource.isAnimatedGIF { return 0.0 } @@ -16,6 +19,9 @@ func CGImageSourceGIFFrameDuration(imageSource: CGImageSource, index: Int) -> NS return duration ?? defaultDuration } +/// Ensures that a duration is never smaller than a threshold value. +/// +/// :returns: A capped frame duration. private func capDuration(duration: Double) -> Double? { if duration < 0 { return .None } let threshold = 0.02 - Double(FLT_EPSILON) @@ -23,6 +29,9 @@ private func capDuration(duration: Double) -> Double? { return cappedDuration } +/// Returns a frame duration from a `GIFProperties` dictionary. +/// +/// :returns: A frame duration. private func durationFromGIFProperties(properties: GIFProperties) -> Double? { let unclampedDelayTime = properties[String(kCGImagePropertyGIFUnclampedDelayTime)] let delayTime = properties[String(kCGImagePropertyGIFDelayTime)] @@ -30,22 +39,36 @@ private func durationFromGIFProperties(properties: GIFProperties) -> Double? { return duration <^> unclampedDelayTime <*> delayTime } +/// Calculates frame duration based on both clamped and unclamped times. +/// +/// :returns: A frame duration. private func duration(unclampedDelayTime: Double)(delayTime: Double) -> Double { let delayArray = [unclampedDelayTime, delayTime] return delayArray.filter(isPositive).first ?? defaultDuration } +/// Checks if a `Double` value is positive. +/// +/// :returns: A boolean value that is `true` if the tested value is positive. private func isPositive(value: Double) -> Bool { return value >= 0 } +/// An extension of `CGImageSourceRef` that add GIF introspection and easier property retrieval. extension CGImageSourceRef { + /// Returns whether the image source contains an animated GIF. + /// + /// :returns: A boolean value that is `true` if the image source contains animated GIF data. var isAnimatedGIF: Bool { let isTypeGIF = UTTypeConformsTo(CGImageSourceGetType(self), kUTTypeGIF) let imageCount = CGImageSourceGetCount(self) return isTypeGIF != 0 && imageCount > 1 } + /// Returns the GIF properties at a specific index. + /// + /// :param: index The index of the GIF properties to retrieve. + /// :returns: A dictionary containing the GIF properties at the passed in index. func GIFPropertiesAtIndex(index: UInt) -> GIFProperties? { if !isAnimatedGIF { return .None } diff --git a/Source/UIImageExtension.swift b/Source/UIImageExtension.swift index 9b9febc..dfa82f8 100644 --- a/Source/UIImageExtension.swift +++ b/Source/UIImageExtension.swift @@ -1,4 +1,10 @@ +/// A `UIImage` extension that makes it easier to resize the image and inspect its size. + extension UIImage { + /// Resizes an image instance. + /// + /// :param: size The new size of the image. + /// :returns: A new resized image instance. func resize(size: CGSize) -> UIImage { UIGraphicsBeginImageContext(size) self.drawInRect(CGRectMake(0, 0, size.width, size.height)) @@ -7,10 +13,19 @@ extension UIImage { return newImage } + /// Returns a new `UIImage` instance using raw image data and a size. + /// + /// :param: data Raw image data. + /// :param: size The size to be used to resize the new image instance. + /// :returns: A new image instance from the passed in data. class func imageWithData(data: NSData, size: CGSize) -> UIImage? { return UIImage(data: data)?.resize(size) } + /// Returns an image size from raw image data. + /// + /// :param: data Raw image data. + /// :returns: The size of the image contained in the data. class func sizeForImageData(data: NSData) -> CGSize? { return UIImage(data: data)?.size }