2015-01-23 00:02:08 +00:00
|
|
|
import UIKit
|
2015-01-22 10:54:27 +00:00
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// A subclass of `UIImageView` that can be animated using an image name string or raw data.
|
2015-06-04 23:24:34 +00:00
|
|
|
public class AnimatableImageView: UIImageView {
|
2016-03-31 17:41:08 +00:00
|
|
|
/// Proxy object for preventing a reference cycle between the CADisplayLink and AnimatableImageView.
|
|
|
|
/// Source: http://merowing.info/2015/11/the-beauty-of-imperfection/
|
2016-06-18 22:56:52 +00:00
|
|
|
fileprivate class TargetProxy {
|
2016-03-31 17:41:08 +00:00
|
|
|
private weak var target: AnimatableImageView?
|
|
|
|
|
|
|
|
init(target: AnimatableImageView) {
|
|
|
|
self.target = target
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func onScreenUpdate() {
|
2016-04-24 09:28:09 +00:00
|
|
|
target?.updateFrameIfNeeded()
|
2016-03-31 17:41:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// An `Animator` instance that holds the frames of a specific image in memory.
|
2015-06-09 21:28:11 +00:00
|
|
|
var animator: Animator?
|
2016-03-31 17:41:08 +00:00
|
|
|
|
2016-03-31 15:12:49 +00:00
|
|
|
/// A flag to avoid invalidating the displayLink on deinit if it was never created
|
|
|
|
private var displayLinkInitialized: Bool = false
|
|
|
|
|
2015-06-04 23:24:34 +00:00
|
|
|
/// A display link that keeps calling the `updateFrame` method on every screen refresh.
|
2016-06-18 22:56:52 +00:00
|
|
|
lazy var displayLink: CADisplayLink = { [unowned self] in
|
2016-03-31 15:12:49 +00:00
|
|
|
self.displayLinkInitialized = true
|
2016-03-31 17:41:08 +00:00
|
|
|
let display = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
|
2016-06-18 22:56:52 +00:00
|
|
|
display.isPaused = true
|
2016-04-07 12:04:22 +00:00
|
|
|
return display
|
|
|
|
}()
|
2015-01-22 10:54:27 +00:00
|
|
|
|
2015-06-04 23:36:47 +00:00
|
|
|
/// The size of the frame cache.
|
|
|
|
public var framePreloadCount = 50
|
2015-06-04 19:38:52 +00:00
|
|
|
|
2016-09-25 09:18:01 +00:00
|
|
|
/// Specifies whether the GIF frames should be pre-scaled to save memory. Default is **false**.
|
|
|
|
public var needsPrescaling = false
|
2016-03-31 17:41:08 +00:00
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// A computed property that returns whether the image view is animating.
|
2015-01-22 10:54:27 +00:00
|
|
|
public var isAnimatingGIF: Bool {
|
2016-06-18 22:56:52 +00:00
|
|
|
return !displayLink.isPaused
|
2015-01-22 10:54:27 +00:00
|
|
|
}
|
|
|
|
|
2016-03-10 22:54:51 +00:00
|
|
|
/// A computed property that returns the total number of frames in the GIF.
|
|
|
|
public var frameCount: Int {
|
|
|
|
return animator?.frameCount ?? 0
|
|
|
|
}
|
2016-03-31 17:41:08 +00:00
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// Prepares the frames using a GIF image file name, without starting the animation.
|
|
|
|
/// The file name should include the `.gif` extension.
|
|
|
|
///
|
2015-06-09 21:28:11 +00:00
|
|
|
/// - parameter imageName: The name of the GIF file. The method looks for the file in the app bundle.
|
2016-06-18 22:56:52 +00:00
|
|
|
public func prepareForAnimation(withGIFNamed imageName: String) {
|
|
|
|
guard let extensionRemoved = imageName.components(separatedBy: ".")[safe: 0],
|
|
|
|
let imagePath = Bundle.main.url(forResource: extensionRemoved, withExtension: "gif"),
|
|
|
|
let data = try? Data(contentsOf: imagePath) else { return }
|
|
|
|
|
|
|
|
prepareForAnimation(withGIFData: data)
|
2015-01-22 10:54:27 +00:00
|
|
|
}
|
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// Prepares the frames using raw GIF image data, without starting the animation.
|
|
|
|
///
|
2015-06-09 21:28:11 +00:00
|
|
|
/// - parameter data: GIF image data.
|
2016-06-18 22:56:52 +00:00
|
|
|
public func prepareForAnimation(withGIFData imageData: Data) {
|
|
|
|
image = UIImage(data: imageData)
|
|
|
|
animator = Animator(data: imageData, size: frame.size, contentMode: contentMode, framePreloadCount: framePreloadCount)
|
2016-03-13 11:30:27 +00:00
|
|
|
animator?.needsPrescaling = needsPrescaling
|
2015-06-04 23:36:47 +00:00
|
|
|
animator?.prepareFrames()
|
2015-06-04 23:24:34 +00:00
|
|
|
attachDisplayLink()
|
2015-01-22 10:54:27 +00:00
|
|
|
}
|
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// Prepares the frames using a GIF image file name and starts animating the image view.
|
|
|
|
///
|
2015-06-09 21:28:11 +00:00
|
|
|
/// - parameter imageName: The name of the GIF file. The method looks for the file in the app bundle.
|
2016-06-18 22:56:52 +00:00
|
|
|
public func animate(withGIFNamed imageName: String) {
|
|
|
|
prepareForAnimation(withGIFNamed: imageName)
|
2015-01-22 10:54:27 +00:00
|
|
|
startAnimatingGIF()
|
|
|
|
}
|
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// Prepares the frames using raw GIF image data and starts animating the image view.
|
|
|
|
///
|
2015-06-09 21:28:11 +00:00
|
|
|
/// - parameter data: GIF image data.
|
2016-06-18 22:56:52 +00:00
|
|
|
public func animate(withGIFData data: Data) {
|
|
|
|
prepareForAnimation(withGIFData: data)
|
2015-01-22 10:54:27 +00:00
|
|
|
startAnimatingGIF()
|
|
|
|
}
|
|
|
|
|
2015-06-09 21:28:11 +00:00
|
|
|
/// Updates the `image` property of the image view if necessary. This method should not be called manually.
|
2016-06-18 22:56:52 +00:00
|
|
|
override public func display(_ layer: CALayer) {
|
2016-04-24 09:28:09 +00:00
|
|
|
image = animator?.currentFrameImage ?? image
|
2015-01-22 10:54:27 +00:00
|
|
|
}
|
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// Starts the image view animation.
|
2015-01-22 10:54:27 +00:00
|
|
|
public func startAnimatingGIF() {
|
2015-06-04 23:24:34 +00:00
|
|
|
if animator?.isAnimatable ?? false {
|
2016-06-18 22:56:52 +00:00
|
|
|
displayLink.isPaused = false
|
2015-06-04 23:24:34 +00:00
|
|
|
}
|
2015-01-22 10:54:27 +00:00
|
|
|
}
|
|
|
|
|
2015-01-23 00:02:08 +00:00
|
|
|
/// Stops the image view animation.
|
2015-01-22 10:54:27 +00:00
|
|
|
public func stopAnimatingGIF() {
|
2016-06-18 22:56:52 +00:00
|
|
|
displayLink.isPaused = true
|
2015-01-22 10:54:27 +00:00
|
|
|
}
|
2016-03-31 17:41:08 +00:00
|
|
|
|
2016-04-24 09:28:09 +00:00
|
|
|
/// Reset the image view values.
|
2016-01-24 19:01:02 +00:00
|
|
|
public func prepareForReuse() {
|
|
|
|
stopAnimatingGIF()
|
|
|
|
animator = nil
|
|
|
|
}
|
2015-06-04 19:38:52 +00:00
|
|
|
|
2016-04-24 09:28:09 +00:00
|
|
|
/// Update the current frame if needed.
|
|
|
|
func updateFrameIfNeeded() {
|
|
|
|
guard let animator = animator else { return }
|
2016-06-18 22:56:52 +00:00
|
|
|
animator.shouldChangeFrame(with: displayLink.duration) { hasNewFrame in
|
2016-04-24 09:28:09 +00:00
|
|
|
if hasNewFrame { self.layer.setNeedsDisplay() }
|
2015-06-09 21:28:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-31 17:41:08 +00:00
|
|
|
/// Invalidate the displayLink so it releases its target.
|
2015-06-09 21:28:11 +00:00
|
|
|
deinit {
|
2016-03-31 15:12:49 +00:00
|
|
|
if displayLinkInitialized {
|
|
|
|
displayLink.invalidate()
|
|
|
|
}
|
2015-06-04 19:38:52 +00:00
|
|
|
}
|
2015-06-04 23:24:34 +00:00
|
|
|
|
2015-06-04 23:36:47 +00:00
|
|
|
/// Attaches the display link.
|
2015-06-09 21:28:11 +00:00
|
|
|
func attachDisplayLink() {
|
2016-06-18 22:56:52 +00:00
|
|
|
displayLink.add(to: .main, forMode: RunLoopMode.commonModes)
|
2015-06-04 23:24:34 +00:00
|
|
|
}
|
2016-03-31 17:41:08 +00:00
|
|
|
}
|