180 lines
4.6 KiB
Swift
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)
|
|
}
|
|
}
|