forked from shadowfacts/Tusker
136 lines
3.9 KiB
Swift
136 lines
3.9 KiB
Swift
//
|
|
// 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<GIFImageView>()
|
|
|
|
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<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) })
|
|
}
|
|
}
|