Add inline docs
This commit is contained in:
parent
b15e7e3399
commit
4ada52c333
|
@ -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 }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/// One of my favorite indian spices.
|
||||
func curry<A, B, C>(f: (A, B) -> C) -> A -> B -> C {
|
||||
return { a in { b in f(a, b) } }
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue