From c172ab92b425612178fe80bcfeed88446606b288 Mon Sep 17 00:00:00 2001 From: Igor Ferreira Date: Fri, 9 Sep 2022 18:38:35 +0200 Subject: [PATCH 1/2] Include new class to control presentation and improve code syntax --- Sources/domain/PresentationController.swift | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Sources/domain/PresentationController.swift diff --git a/Sources/domain/PresentationController.swift b/Sources/domain/PresentationController.swift new file mode 100644 index 0000000..f53dc77 --- /dev/null +++ b/Sources/domain/PresentationController.swift @@ -0,0 +1,59 @@ +// +// 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, loop: Binding, 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)) + } +} From fd781bc5dbfe3a46b192925caaf88045fe3eb8de Mon Sep 17 00:00:00 2001 From: Igor Ferreira Date: Fri, 9 Sep 2022 18:38:50 +0200 Subject: [PATCH 2/2] Take advantage of new class --- Sources/GIFImage.swift | 52 ++++++++++++------------------------------ 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/Sources/GIFImage.swift b/Sources/GIFImage.swift index 0ceb97b..3d694e4 100644 --- a/Sources/GIFImage.swift +++ b/Sources/GIFImage.swift @@ -1,7 +1,5 @@ import SwiftUI -private let kDefaultGIFFrameInterval: TimeInterval = 1.0 / 24.0 - /// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source /// into frames and stream them based in the "Delay" key packaged on which frame item. The view will use the `ImageLoader` from the environment /// to convert the fetch the `Data` @@ -9,8 +7,7 @@ public struct GIFImage: View { public let source: GIFSource public let placeholder: RawImage public let errorImage: RawImage? - public let frameRate: FrameRate - private let action: (GIFSource) async throws -> Void + private let presentationController: PresentationController @Environment(\.imageLoader) var imageLoader @State @MainActor private var frame: RawImage? @@ -74,8 +71,14 @@ public struct GIFImage: View { self._loop = loop self.placeholder = placeholder self.errorImage = errorImage - self.frameRate = frameRate - self.action = loopAction + + self.presentationController = PresentationController( + source: source, + frameRate: frameRate, + animate: animate, + loop: loop, + action: loopAction + ) } public var body: some View { @@ -97,39 +100,12 @@ public struct GIFImage: View { } @Sendable private func load() { - guard animate else { return } presentationTask?.cancel() - presentationTask = Task { - do { - repeat { - for try await imageFrame in try await imageLoader.load(source: source) { - try await update(imageFrame) - if !animate { break } - } - try await action(source) - } while(self.loop && self.animate) - } catch { - if !(error is CancellationError) { - await setFrame(errorImage ?? placeholder) - } - } - } - } - - @Sendable private func update(_ imageFrame: ImageFrame) async throws { - await setFrame(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)) + presentationTask = Task { await presentationController.start( + imageLoader: imageLoader, + fallbackImage: errorImage ?? placeholder, + frameUpdate: setFrame(_:) + )} } @MainActor