Skip to content

Commit

Permalink
Merge pull request #51 from igorcferreira/feature/code_optimisation
Browse files Browse the repository at this point in the history
Feature/code optimisation
  • Loading branch information
igorcferreira authored Sep 9, 2022
2 parents 25f8b35 + fd781bc commit 1b3bb89
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 38 deletions.
52 changes: 14 additions & 38 deletions Sources/GIFImage.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
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`
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?
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
59 changes: 59 additions & 0 deletions Sources/domain/PresentationController.swift
Original file line number Diff line number Diff line change
@@ -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<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))
}
}

0 comments on commit 1b3bb89

Please sign in to comment.