Gifu/Source/Animator.swift

110 lines
4.0 KiB
Swift
Raw Normal View History

import UIKit
import ImageIO
2015-01-20 19:44:15 +00:00
import Runes
2015-01-23 00:02:08 +00:00
/// Responsible for storing and updating the frames of a `AnimatableImageView` instance via delegation.
2015-01-22 10:54:27 +00:00
class Animator: NSObject {
2015-01-23 00:02:08 +00:00
/// The animator delegate. Should conform to the `Animatable` protocol.
2015-01-22 10:54:27 +00:00
let delegate: Animatable
2015-01-23 00:02:08 +00:00
/// 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
2015-01-23 00:02:08 +00:00
/// Time elapsed since the last frame change. Used to determine when the frame should be updated.
private var timeSinceLastFrameChange: NSTimeInterval = 0.0
2015-01-23 00:02:08 +00:00
/// A display link that keeps calling the `updateCurrentFrame` method on every screen refresh.
2015-01-22 10:54:27 +00:00
private lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: "updateCurrentFrame")
2015-01-23 00:02:08 +00:00
/// The current image frame to show.
var currentFrame: UIImage? {
return frameAtIndex(currentFrameIndex)
}
2015-01-23 00:02:08 +00:00
/// Returns whether the animator is animating.
2015-01-22 10:54:27 +00:00
var isAnimating: Bool {
return !displayLink.paused
}
2015-01-23 00:02:08 +00:00
/// 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.
2015-01-22 10:54:27 +00:00
required init(data: NSData, delegate: Animatable) {
let imageSource = CGImageSourceCreateWithData(data, nil)
2015-01-22 10:54:27 +00:00
self.delegate = delegate
super.init()
attachDisplayLink()
2015-01-22 10:54:27 +00:00
curry(prepareFrames) <^> imageSource <*> delegate.frame.size
pauseAnimation()
}
2015-01-22 10:54:27 +00:00
// MARK: - Frames
2015-01-23 00:02:08 +00:00
/// 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)
2014-12-12 21:49:15 +00:00
(animatedFrames, totalDuration) = reduce(0..<numberOfFrames, ([AnimatedFrame](), 0.0)) { accumulator, index in
let accumulatedFrames = accumulator.0
let accumulatedDuration = accumulator.1
let frameDuration = CGImageSourceGIFFrameDuration(imageSource, index)
2015-04-08 21:24:45 +00:00
let frameImageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil)
let frame = UIImage(CGImage: frameImageRef)?.resize(size)
let animatedFrame = AnimatedFrame(image: frame, duration: frameDuration)
2014-12-12 21:49:15 +00:00
return (accumulatedFrames + [animatedFrame], accumulatedDuration + frameDuration)
}
}
2015-01-23 00:02:08 +00:00
/// 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
}
2015-01-23 00:02:08 +00:00
/// 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.
2015-02-10 19:07:54 +00:00
func updateCurrentFrame() {
2015-01-22 10:54:27 +00:00
if totalDuration == 0 { return }
timeSinceLastFrameChange += min(maxTimeStep, displayLink.duration)
var frameDuration = animatedFrames[currentFrameIndex].duration
if timeSinceLastFrameChange >= frameDuration {
timeSinceLastFrameChange -= frameDuration
currentFrameIndex = ++currentFrameIndex % animatedFrames.count
2015-01-22 10:54:27 +00:00
delegate.layer.setNeedsDisplay()
}
}
// MARK: - Animation
2015-01-23 00:02:08 +00:00
/// Pauses the display link.
func pauseAnimation() {
displayLink.paused = true
}
2015-01-23 00:02:08 +00:00
/// Resumes the display link.
func resumeAnimation() {
2015-01-22 10:54:27 +00:00
if totalDuration > 0 {
displayLink.paused = false
}
}
2015-01-23 00:02:08 +00:00
/// Attaches the dsiplay link.
2015-01-22 10:54:27 +00:00
func attachDisplayLink() {
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
}