fix gif caching

This commit is contained in:
leminlimez committed 2025-07-19 08:58:08 -04:00
1 parent 71b1d0a21d
commit 395cbe5e13
19 files changed
+728 -475

No files matched your search

@@ -0,0 +1,46 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
import ImageIO
public actor CGImageSourceFrameSequence: AsyncSequence {
public typealias Element = ImageFrame
public enum LoadError: Error {
case invalidData
case emptyData
}
public let source: CGImageSource
public init(data: Data) throws {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
throw LoadError.invalidData
}
switch CGImageSourceGetStatus(source) {
case .statusComplete: break
case .statusReadingHeader: break
case .statusIncomplete: throw LoadError.emptyData
case .statusInvalidData: throw LoadError.invalidData
case .statusUnexpectedEOF: throw LoadError.invalidData
case .statusUnknownType: throw LoadError.invalidData
@unknown default: throw LoadError.invalidData
}
self.init(source: source)
}
public init(source: CGImageSource) {
self.source = source
}
nonisolated public func makeAsyncIterator() -> CGImageSourceIterator {
CGImageSourceIterator(source: source)
}
}
@@ -0,0 +1,36 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
import ImageIO
public actor CGImageSourceIterator: AsyncIteratorProtocol {
public let frameCount: Int
public let source: CGImageSource
public private(set) var currentFrame: Int
public init(source: CGImageSource) {
self.source = source
self.frameCount = CGImageSourceGetCount(source)
self.currentFrame = 0
}
public func next() async throws -> ImageFrame? {
guard currentFrame < frameCount else {
return nil
}
let frame: ImageFrame? = if let image = CGImageSourceCreateImageAtIndex(source, currentFrame, nil) {
ImageFrame(image: image, interval: source.intervalAtIndex(currentFrame))
} else {
nil
}
currentFrame += 1
return frame
}
}
@@ -0,0 +1,80 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
/// Object responsible to load a `GIFSource` into a `ImageFrame` stream.
public struct ImageLoader {
public let session: URLSession
public let cache: URLCache
public let fileManager: FileManager
public init(session: URLSession = .shared, cache: URLCache = .shared, fileManager: FileManager = .default) {
self.session = session
self.cache = cache
self.fileManager = fileManager
}
public func load(source: GIFSource) async throws -> CGImageSourceFrameSequence {
let data = try await source.loadData(session: session, cache: cache, fileManager: fileManager)
return try data.imageAsyncSequence()
}
}
private extension GIFSource {
func loadData(session: URLSession, cache: URLCache, fileManager: FileManager) async throws -> Data {
switch self {
case let .static(data): data
case .remote(let url), .remoteURL(let url): try await url.loadData(session: session, cache: cache)
case let .remoteRequest(request): try await request.loadData(session: session, cache: cache)
case let .local(filePath): try await filePath.loadData(fileManager: fileManager)
}
}
}
private extension String {
func loadData(fileManager: FileManager) async throws -> Data {
guard fileManager.fileExists(atPath: self) else {
throw URLError(.fileDoesNotExist)
}
guard let data = fileManager.contents(atPath: self) else {
throw URLError(.cannotOpenFile)
}
return data
}
}
private extension URL {
func loadData(session: URLSession, cache: URLCache) async throws -> Data {
let request = URLRequest(url: self)
return try await request.loadData(session: session, cache: cache)
}
}
private extension URLRequest {
func loadData(session: URLSession, cache: URLCache) async throws -> Data {
if let cache = cache.cachedResponse(for: self) {
return cache.data
}
let (data, response) = try await session.data(for: self)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard httpResponse.isSuccess else {
throw URLError(.init(rawValue: httpResponse.statusCode))
}
cache.storeCachedResponse(CachedURLResponse(response: response, data: data), for: self)
return data
}
}
private extension HTTPURLResponse {
var isSuccess: Bool {
200..<300 ~= statusCode
}
}
@@ -0,0 +1,65 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 9/9/22.
//
import Foundation
import SwiftUI
private let kDefaultGIFFrameInterval: TimeInterval = 1.0 / 24.0
struct PresentationController {
let source: GIFSource
let frameRate: FrameRate
let action: (GIFSource) async throws -> Void
@Binding var animate: Bool
@Binding var loop: Bool
init(
source: GIFSource,
frameRate: FrameRate,
animate: Binding<Bool>,
loop: Binding<Bool>,
action: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.source = source
self.action = action
self.frameRate = frameRate
self._animate = animate
self._loop = loop
}
func start(imageLoader: ImageLoader, fallbackImage: RawImage, frameUpdate: (RawImage) async -> Void) async {
do {
repeat {
for try await imageFrame in try await imageLoader.load(source: source) {
try await update(imageFrame, frameUpdate: frameUpdate)
if !animate { break }
}
try await action(source)
} while(self.loop && self.animate)
} catch {
if !(error is CancellationError) {
await frameUpdate(fallbackImage)
}
}
}
private func update(_ imageFrame: ImageFrame, frameUpdate: (RawImage) async -> Void) async throws {
await frameUpdate(RawImage.create(with: imageFrame.image))
let calculatedInterval = imageFrame.interval ?? kDefaultGIFFrameInterval
let interval: Double
switch frameRate {
case .static(let fps):
interval = (1.0 / Double(fps))
case .limited(let fps):
let intervalLimit = (1.0 / Double(fps))
interval = max(calculatedInterval, intervalLimit)
case .dynamic:
interval = imageFrame.interval ?? kDefaultGIFFrameInterval
}
try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000.0))
}
}