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.. 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) } }