// // GIFImageView.swift // TuskerComponents // // Created by Shadowfacts on 11/11/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit open class GIFImageView: UIImageView { public fileprivate(set) var gifController: GIFController? = nil public 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. public func detachGIFController() { gifController?.detach(from: self) } } /// A `GIFController` controls the animation of one or more `GIFImageView`s. public class GIFController { // GIFImageView strongly holds the controller so that when the last view detaches, the controller is freed private var imageViews = WeakArray() public let gifData: Data private(set) var state: State = .stopped public private(set) var lastFrame: (image: UIImage, index: Int)? = nil public 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. public 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. public func detach(from view: GIFImageView) { // todo: does === work the way i want here imageViews.removeAll(where: { $0 === view }) view.gifController = nil } public 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 } } public func stopAnimating() { guard state == .playing else { return } state = .stopping } enum State: Equatable { case stopped, playing, stopping var shouldStop: Bool { self == .stopped || self == .stopping } } } private class WeakHolder { weak var object: T? init(_ object: T?) { self.object = object } } private struct WeakArray: MutableCollection, RangeReplaceableCollection { private var array: [WeakHolder] var startIndex: Int { array.startIndex } var endIndex: Int { array.endIndex } init() { array = [] } init(_ elements: [Element]) { array = elements.map { WeakHolder($0) } } init(_ elements: [Element?]) { array = elements.map { WeakHolder($0) } } subscript(position: Int) -> Element? { get { array[position].object } set(newValue) { array[position] = WeakHolder(newValue) } } func index(after i: Int) -> Int { return array.index(after: i) } mutating func replaceSubrange(_ subrange: Range, with newElements: C) where C : Collection, Self.Element == C.Element { array.replaceSubrange(subrange, with: newElements.map { WeakHolder($0) }) } }