Files
GIFCollector/GIFCollector MessagesExtension/Views/GIFPlayerView.swift
Joshua Higgins 4ef60608cd init, finished
2025-06-02 17:49:33 -04:00

180 lines
4.6 KiB
Swift

import UIKit
import ImageIO
class GIFPlayerView: UIView {
private var imageView: UIImageView!
private var displayLink: CADisplayLink?
private var imageSource: CGImageSource?
private var frameCount: Int = 0
private var currentFrameIndex: Int = 0
private var frameDurations: [TimeInterval] = []
private var totalDuration: TimeInterval = 0
private var currentTime: TimeInterval = 0
private var previousTimestamp: TimeInterval = 0
// Default configuration
private var loopCount: Int = 0 // 0 means loop forever
private var currentLoopCount: Int = 0
// Public properties
var isPlaying: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
deinit {
stopAnimating()
}
private func commonInit() {
imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
// MARK: - Public Methods
func loadGIF(from data: Data) {
stopAnimating()
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return }
imageSource = source
// Get frame count
frameCount = CGImageSourceGetCount(source)
guard frameCount > 0 else { return }
// Calculate frame durations
frameDurations.removeAll()
totalDuration = 0
for i in 0..<frameCount {
let duration = frameDurationAtIndex(i)
frameDurations.append(duration)
totalDuration += duration
}
// Reset state
currentFrameIndex = 0
currentTime = 0
// Display first frame
if let image = imageAtIndex(0) {
imageView.image = image
}
}
func startAnimating() {
guard !isPlaying, frameCount > 1 else { return }
isPlaying = true
previousTimestamp = CACurrentMediaTime()
displayLink = CADisplayLink(target: self, selector: #selector(updateFrame))
displayLink?.add(to: .main, forMode: .common)
}
func stopAnimating() {
isPlaying = false
displayLink?.invalidate()
displayLink = nil
}
func loadGIF(from url: URL, completion: @escaping (Bool) -> Void) {
URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let self = self, let data = data, error == nil else {
DispatchQueue.main.async {
completion(false)
}
return
}
DispatchQueue.main.async {
self.loadGIF(from: data)
completion(true)
}
}.resume()
}
// MARK: - Private Methods
@objc private func updateFrame(displayLink: CADisplayLink) {
let timestamp = CACurrentMediaTime()
let elapsed = timestamp - previousTimestamp
previousTimestamp = timestamp
currentTime += elapsed
// Check if we need to show the next frame
while currentTime >= frameDurations[currentFrameIndex], frameCount > 0 {
currentTime -= frameDurations[currentFrameIndex]
currentFrameIndex = (currentFrameIndex + 1) % frameCount
// Handle loop count
if currentFrameIndex == 0 && loopCount > 0 {
currentLoopCount += 1
if currentLoopCount >= loopCount {
stopAnimating()
break
}
}
// Update the image
if let image = imageAtIndex(currentFrameIndex) {
imageView.image = image
}
}
}
private func imageAtIndex(_ index: Int) -> UIImage? {
guard let source = imageSource, index < frameCount,
let cgImage = CGImageSourceCreateImageAtIndex(source, index, nil) else {
return nil
}
return UIImage(cgImage: cgImage)
}
private func frameDurationAtIndex(_ index: Int) -> TimeInterval {
guard let source = imageSource, index < frameCount else {
return 0.1 // Default duration
}
// Get frame properties
guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [String: Any],
let gifProperties = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] else {
return 0.1 // Default duration
}
// Get delay time
var delayTime: TimeInterval = 0.1 // Default duration
if let unclampedDelay = gifProperties[kCGImagePropertyGIFUnclampedDelayTime as String] as? TimeInterval,
unclampedDelay > 0 {
delayTime = unclampedDelay
} else if let delay = gifProperties[kCGImagePropertyGIFDelayTime as String] as? TimeInterval,
delay > 0 {
delayTime = delay
}
// Clamp to minimum delay (ensures reasonable frame rate)
return max(delayTime, 0.02)
}
}