// // 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() 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 } } }