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 {
|
protocol Animatable {
|
||||||
var layer: CALayer { get }
|
var layer: CALayer { get }
|
||||||
var frame: CGRect { get }
|
var frame: CGRect { get }
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) } }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue