Gifu/Source/Classes/GIFAnimatable.swift

206 lines
7.4 KiB
Swift

import Foundation
import UIKit
/// The protocol that view classes need to conform to to enable animated GIF support.
public protocol GIFAnimatable: class {
/// Responsible for managing the animation frames.
var animator: Animator? { get set }
/// Notifies the instance that it needs display.
var layer: CALayer { get }
/// View frame used for resizing the frames.
var frame: CGRect { get set }
/// Content mode used for resizing the frames.
var contentMode: UIView.ContentMode { get set }
}
/// A single-property protocol that animatable classes can optionally conform to.
public protocol ImageContainer {
/// Used for displaying the animation frames.
var image: UIImage? { get set }
}
extension GIFAnimatable where Self: ImageContainer {
/// Returns the intrinsic content size based on the size of the image.
public var intrinsicContentSize: CGSize {
return image?.size ?? CGSize.zero
}
}
extension GIFAnimatable {
/// Total duration of one animation loop
public var gifLoopDuration: TimeInterval {
return animator?.loopDuration ?? 0
}
/// Returns the active frame if available.
public var activeFrame: UIImage? {
return animator?.activeFrame()
}
/// Total frame count of the GIF.
public var frameCount: Int {
return animator?.frameCount ?? 0
}
/// Introspect whether the instance is animating.
public var isAnimatingGIF: Bool {
return animator?.isAnimating ?? false
}
/// Prepare for animation and start animating immediately.
///
/// - parameter imageName: The file name of the GIF in the main bundle.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
/// - parameter completionHandler: Completion callback function
public func animate(withGIFNamed imageName: String, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
animator?.animate(withGIFNamed: imageName,
size: frame.size,
contentMode: contentMode,
loopCount: loopCount,
completionHandler: completionHandler)
}
/// Prepare for animation and start animating immediately.
///
/// - parameter imageData: GIF image data.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
/// - parameter completionHandler: Completion callback function
public func animate(withGIFData imageData: Data, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
animator?.animate(withGIFData: imageData,
size: frame.size,
contentMode: contentMode,
loopCount: loopCount,
completionHandler: completionHandler)
}
/// Prepare for animation and start animating immediately.
///
/// - parameter imageURL: GIF image url.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
/// - parameter completionHandler: Completion callback function
public func animate(withGIFURL imageURL: URL, loopCount: Int = 0, completionHandler: (() -> Void)? = nil) {
let session = URLSession.shared
let task = session.dataTask(with: imageURL) { (data, response, error) in
switch (data, response, error) {
case (.none, _, let error?):
print("Error downloading gif:", error.localizedDescription, "at url:", imageURL.absoluteString)
case (let data?, _, _):
DispatchQueue.main.async {
self.animate(withGIFData: data, loopCount: loopCount, completionHandler: completionHandler)
}
default: ()
}
}
task.resume()
}
/// Prepares the animator instance for animation.
///
/// - parameter imageName: The file name of the GIF in the main bundle.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
public func prepareForAnimation(withGIFNamed imageName: String,
loopCount: Int = 0,
completionHandler: (() -> Void)? = nil) {
animator?.prepareForAnimation(withGIFNamed: imageName,
size: frame.size,
contentMode: contentMode,
loopCount: loopCount,
completionHandler: completionHandler)
}
/// Prepare for animation and start animating immediately.
///
/// - parameter imageData: GIF image data.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
public func prepareForAnimation(withGIFData imageData: Data,
loopCount: Int = 0,
completionHandler: (() -> Void)? = nil) {
if var imageContainer = self as? ImageContainer {
imageContainer.image = UIImage(data: imageData)
}
animator?.prepareForAnimation(withGIFData: imageData,
size: frame.size,
contentMode: contentMode,
loopCount: loopCount,
completionHandler: completionHandler)
}
/// Prepare for animation and start animating immediately.
///
/// - parameter imageURL: GIF image url.
/// - parameter loopCount: Desired number of loops, <= 0 for infinite loop.
public func prepareForAnimation(withGIFURL imageURL: URL,
loopCount: Int = 0,
completionHandler: (() -> Void)? = nil) {
let session = URLSession.shared
let task = session.dataTask(with: imageURL) { (data, response, error) in
switch (data, response, error) {
case (.none, _, let error?):
print("Error downloading gif:", error.localizedDescription, "at url:", imageURL.absoluteString)
case (let data?, _, _):
DispatchQueue.main.async {
self.prepareForAnimation(withGIFData: data,
loopCount: loopCount,
completionHandler: completionHandler)
}
default: ()
}
}
task.resume()
}
/// Stop animating and free up GIF data from memory.
public func prepareForReuse() {
animator?.prepareForReuse()
}
/// Start animating GIF.
public func startAnimatingGIF() {
animator?.startAnimating()
}
/// Stop animating GIF.
public func stopAnimatingGIF() {
animator?.stopAnimating()
}
/// Whether the frame images should be resized or not. The default is `false`, which means that the frame images retain their original size.
///
/// - parameter resize: Boolean value indicating whether individual frames should be resized.
public func setShouldResizeFrames(_ resize: Bool) {
animator?.shouldResizeFrames = resize
}
/// Sets the number of frames that should be buffered. Default is 50. A high number will result in more memory usage and less CPU load, and vice versa.
///
/// - parameter frames: The number of frames to buffer.
public func setFrameBufferCount(_ frames: Int) {
animator?.frameBufferCount = frames
}
/// Updates the image with a new frame if necessary.
public func updateImageIfNeeded() {
if var imageContainer = self as? ImageContainer {
let container = imageContainer
imageContainer.image = activeFrame ?? container.image
} else {
layer.contents = activeFrame?.cgImage
}
}
}
extension GIFAnimatable {
/// Calls setNeedsDisplay on the layer whenever the animator has a new frame. Should *not* be called directly.
func animatorHasNewFrame() {
layer.setNeedsDisplay()
}
}