Add inline docs

This commit is contained in:
Reda Lemeden 2015-01-23 01:02:08 +01:00
parent b15e7e3399
commit 4ada52c333
7 changed files with 97 additions and 8 deletions

View File

@ -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 { protocol Animatable {
var layer: CALayer { get } var layer: CALayer { get }
var frame: CGRect { get } var frame: CGRect { get }

View File

@ -1,42 +1,61 @@
import UIKit
import ImageIO import ImageIO
import Runes 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 { public class AnimatableImageView: UIImageView, Animatable {
/// An `Animator` instance that holds the frames of a specific image in memory.
var animator: Animator? var animator: Animator?
/// A computed property that returns whether the image view is animating.
public var isAnimatingGIF: Bool { public var isAnimatingGIF: Bool {
return animator?.isAnimating ?? isAnimating() 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) { public func prepareForAnimation(imageNamed imageName: String) {
let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(imageName) let path = NSBundle.mainBundle().bundlePath.stringByAppendingPathComponent(imageName)
prepareForAnimation <^> NSData(contentsOfFile: path) 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) { public func prepareForAnimation(imageData data: NSData) {
image = UIImage(data: data) image = UIImage(data: data)
animator = Animator(data: data, delegate: self) 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) { public func animateWithImage(named imageName: String) {
prepareForAnimation(imageNamed: imageName) prepareForAnimation(imageNamed: imageName)
startAnimatingGIF() 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) { public func animateWithImageData(#data: NSData) {
prepareForAnimation(imageData: data) prepareForAnimation(imageData: data)
startAnimatingGIF() startAnimatingGIF()
} }
/// Updates the `UIImage` property of the image view if necessary. This method should not be called manually.
override public func displayLayer(layer: CALayer!) { override public func displayLayer(layer: CALayer!) {
image = animator?.currentFrame? image = animator?.currentFrame?
} }
/// Starts the image view animation.
public func startAnimatingGIF() { public func startAnimatingGIF() {
animator?.resumeAnimation() ?? startAnimating() animator?.resumeAnimation() ?? startAnimating()
} }
/// Stops the image view animation.
public func stopAnimatingGIF() { public func stopAnimatingGIF() {
animator?.pauseAnimation() ?? stopAnimating() animator?.pauseAnimation() ?? stopAnimating()
} }

View File

@ -1,3 +1,4 @@
/// Keeps a reference to an `UIImage` instance and its duration as a GIF frame.
struct AnimatedFrame { struct AnimatedFrame {
let image: UIImage? let image: UIImage?
let duration: NSTimeInterval let duration: NSTimeInterval

View File

@ -2,23 +2,37 @@ import UIKit
import ImageIO import ImageIO
import Runes import Runes
/// Responsible for storing and updating the frames of a `AnimatableImageView` instance via delegation.
class Animator: NSObject { class Animator: NSObject {
let maxTimeStep = 1.0 /// The animator delegate. Should conform to the `Animatable` protocol.
var animatedFrames = [AnimatedFrame]()
var totalDuration: NSTimeInterval = 0.0
let delegate: Animatable 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 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 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") private lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: "updateCurrentFrame")
/// The current image frame to show.
var currentFrame: UIImage? { var currentFrame: UIImage? {
return frameAtIndex(currentFrameIndex) return frameAtIndex(currentFrameIndex)
} }
/// Returns whether the animator is animating.
var isAnimating: Bool { var isAnimating: Bool {
return !displayLink.paused 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) { required init(data: NSData, delegate: Animatable) {
let imageSource = CGImageSourceCreateWithData(data, nil) let imageSource = CGImageSourceCreateWithData(data, nil)
self.delegate = delegate self.delegate = delegate
@ -29,6 +43,10 @@ class Animator: NSObject {
} }
// MARK: - Frames // 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) { private func prepareFrames(imageSource: CGImageSourceRef, size: CGSize) {
let numberOfFrames = Int(CGImageSourceGetCount(imageSource)) let numberOfFrames = Int(CGImageSourceGetCount(imageSource))
animatedFrames.reserveCapacity(numberOfFrames) 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 } if index >= animatedFrames.count { return .None }
return animatedFrames[index].image 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 } if totalDuration == 0 { return }
timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration) timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration)
@ -65,16 +90,19 @@ class Animator: NSObject {
} }
// MARK: - Animation // MARK: - Animation
/// Pauses the display link.
func pauseAnimation() { func pauseAnimation() {
displayLink.paused = true displayLink.paused = true
} }
/// Resumes the display link.
func resumeAnimation() { func resumeAnimation() {
if totalDuration > 0 { if totalDuration > 0 {
displayLink.paused = false displayLink.paused = false
} }
} }
/// Attaches the dsiplay link.
func attachDisplayLink() { func attachDisplayLink() {
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes) displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
} }

View File

@ -1,3 +1,4 @@
/// One of my favorite indian spices.
func curry<A, B, C>(f: (A, B) -> C) -> A -> B -> C { func curry<A, B, C>(f: (A, B) -> C) -> A -> B -> C {
return { a in { b in f(a, b) } } return { a in { b in f(a, b) } }
} }

View File

@ -1,11 +1,14 @@
import UIKit
import ImageIO import ImageIO
import MobileCoreServices import MobileCoreServices
import Runes import Runes
import UIKit
internal typealias GIFProperties = [String : Double] typealias GIFProperties = [String : Double]
private let defaultDuration: Double = 0 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 { func CGImageSourceGIFFrameDuration(imageSource: CGImageSource, index: Int) -> NSTimeInterval {
if !imageSource.isAnimatedGIF { return 0.0 } if !imageSource.isAnimatedGIF { return 0.0 }
@ -16,6 +19,9 @@ func CGImageSourceGIFFrameDuration(imageSource: CGImageSource, index: Int) -> NS
return duration ?? defaultDuration 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? { private func capDuration(duration: Double) -> Double? {
if duration < 0 { return .None } if duration < 0 { return .None }
let threshold = 0.02 - Double(FLT_EPSILON) let threshold = 0.02 - Double(FLT_EPSILON)
@ -23,6 +29,9 @@ private func capDuration(duration: Double) -> Double? {
return cappedDuration return cappedDuration
} }
/// Returns a frame duration from a `GIFProperties` dictionary.
///
/// :returns: A frame duration.
private func durationFromGIFProperties(properties: GIFProperties) -> Double? { private func durationFromGIFProperties(properties: GIFProperties) -> Double? {
let unclampedDelayTime = properties[String(kCGImagePropertyGIFUnclampedDelayTime)] let unclampedDelayTime = properties[String(kCGImagePropertyGIFUnclampedDelayTime)]
let delayTime = properties[String(kCGImagePropertyGIFDelayTime)] let delayTime = properties[String(kCGImagePropertyGIFDelayTime)]
@ -30,22 +39,36 @@ private func durationFromGIFProperties(properties: GIFProperties) -> Double? {
return duration <^> unclampedDelayTime <*> delayTime 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 { private func duration(unclampedDelayTime: Double)(delayTime: Double) -> Double {
let delayArray = [unclampedDelayTime, delayTime] let delayArray = [unclampedDelayTime, delayTime]
return delayArray.filter(isPositive).first ?? defaultDuration 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 { private func isPositive(value: Double) -> Bool {
return value >= 0 return value >= 0
} }
/// An extension of `CGImageSourceRef` that add GIF introspection and easier property retrieval.
extension CGImageSourceRef { 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 { var isAnimatedGIF: Bool {
let isTypeGIF = UTTypeConformsTo(CGImageSourceGetType(self), kUTTypeGIF) let isTypeGIF = UTTypeConformsTo(CGImageSourceGetType(self), kUTTypeGIF)
let imageCount = CGImageSourceGetCount(self) let imageCount = CGImageSourceGetCount(self)
return isTypeGIF != 0 && imageCount > 1 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? { func GIFPropertiesAtIndex(index: UInt) -> GIFProperties? {
if !isAnimatedGIF { return .None } if !isAnimatedGIF { return .None }

View File

@ -1,4 +1,10 @@
/// A `UIImage` extension that makes it easier to resize the image and inspect its size.
extension UIImage { 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 { func resize(size: CGSize) -> UIImage {
UIGraphicsBeginImageContext(size) UIGraphicsBeginImageContext(size)
self.drawInRect(CGRectMake(0, 0, size.width, size.height)) self.drawInRect(CGRectMake(0, 0, size.width, size.height))
@ -7,10 +13,19 @@ extension UIImage {
return newImage 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? { class func imageWithData(data: NSData, size: CGSize) -> UIImage? {
return UIImage(data: data)?.resize(size) 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? { class func sizeForImageData(data: NSData) -> CGSize? {
return UIImage(data: data)?.size return UIImage(data: data)?.size
} }