Tusker/Packages/TuskerComponents/Sources/TuskerComponents/GIFImageView.swift

136 lines
3.9 KiB
Swift
Raw Normal View History

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