Tusker/Tusker/Views/GIFImageView.swift

92 lines
2.7 KiB
Swift

//
// GIFImageView.swift
// Tusker
//
// Created by Shadowfacts on 11/11/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
class GIFImageView: UIImageView {
fileprivate(set) var gifController: GIFController? = nil
var isAnimatingGIF: Bool { gifController?.state == .playing }
/// Detaches the current GIF controller from this view.
/// If this view is the GIF controller's only one, it will stop itself.
func detachGIFController() {
gifController?.detach(from: self)
}
}
/// A `GIFController` controls the animation of one or more `GIFImageView`s.
class GIFController {
// GIFImageView strongly holds the controller so that when the last view detaches, the controller is freed
private var imageViews = WeakArray<GIFImageView>()
private(set) var gifData: Data
private(set) var state: State = .stopped
private(set) var lastFrame: (image: UIImage, index: Int)? = nil
init(gifData: Data) {
self.gifData = gifData
}
/// Attaches another view to this controller, letting it play back alongside the others.
/// Immediately brings it into sync with the others, setting the last frame if there was one.
func attach(to view: GIFImageView) {
imageViews.append(view)
view.gifController = self
if let lastFrame = lastFrame {
view.image = lastFrame.image
}
}
/// Detaches the given view from this controller.
/// If no views attached views remain, the last strong reference to this controller is nilled out
/// and image animation will stop at the next CGAnimateImageDataWithBlock callback.
func detach(from view: GIFImageView) {
// todo: does === work the way i want here
imageViews.removeAll(where: { $0 === view })
view.gifController = nil
}
func startAnimating() {
guard state.shouldStop else { return }
state = .playing
CGAnimateImageDataWithBlock(gifData as CFData, nil) { [weak self] (frameIndex, cgImage, stop) in
guard let self = self else {
stop.pointee = true
return
}
let image = UIImage(cgImage: cgImage)
self.lastFrame = (image, frameIndex)
for case let .some(view) in self.imageViews {
view.image = image
}
stop.pointee = self.state.shouldStop
}
}
func stopAnimating() {
guard state == .playing else { return }
state = .stopping
}
enum State: Equatable {
case stopped, playing, stopping
var shouldStop: Bool {
self == .stopped || self == .stopping
}
}
}