Skip to content

Commit

Permalink
Improve control of loop variable
Browse files Browse the repository at this point in the history
  • Loading branch information
igorcferreira committed Sep 9, 2022
1 parent 46b033c commit 062778f
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 27 deletions.
23 changes: 14 additions & 9 deletions Sample.swiftpm/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,22 @@ struct ContentView: View {
]
@State var placeholder = UIImage(systemName: "photo.circle.fill")!
@State var error: UIImage?
@State var loop: Bool = true

var body: some View {
List(items) { item in
GIFImage(
source: item.source,
loop: true,
placeholder: placeholder,
errorImage: error,
frameRate: .dynamic,
loopAction: loopAction(source:)
).frame(width: 310.0, height: 175.0, alignment: .center)

VStack {
Toggle("Loop", isOn: $loop).padding([.leading, .trailing])
List(items) { item in
GIFImage(
source: item.source,
loop: $loop,
placeholder: placeholder,
errorImage: error,
frameRate: .dynamic,
loopAction: loopAction(source:)
).frame(width: 310.0, height: 175.0, alignment: .center)
}
}
}

Expand Down
59 changes: 58 additions & 1 deletion Sources/GIFImage+Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SwiftUI

public extension GIFImage {

/// `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.
///
Expand All @@ -20,7 +21,35 @@ public extension GIFImage {
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
init?(
url: String,
loop: Bool = true,
loop: Bool,
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
url: url,
loop: .constant(loop),
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}

/// `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.
///
/// - Parameters:
/// - url: URL string of the image. If the string cannot be parsed into URL, this constructor early aborts and returns `nil`
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
init?(
url: String,
loop: Binding<Bool> = Binding.constant(true),
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
Expand Down Expand Up @@ -56,6 +85,34 @@ public extension GIFImage {
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
url: url,
loop: .constant(loop),
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}

/// `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.
///
/// - Parameters:
/// - url: URL of the image. The response is cached using `URLCache`
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
init(
url: URL,
loop: Binding<Bool> = Binding.constant(true),
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
source: GIFSource.remote(url: url),
Expand Down
70 changes: 54 additions & 16 deletions Sources/GIFImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,46 @@ private let kDefaultGIFFrameInterval: TimeInterval = 1.0 / 24.0
/// 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 loop: Bool
public let placeholder: RawImage
public let errorImage: RawImage?
public let frameRate: FrameRate
private let action: (GIFSource) async throws -> Void

@Environment(\.imageLoader) var imageLoader
@State private var frame: RawImage?
@Binding public var loop: Bool
@State private var presentationTask: Task<(), Never>? = nil


/// `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.
///
/// - Parameters:
/// - source: Source of the image. If the source is remote, the response is cached using `URLCache`
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
public init(
source: GIFSource,
loop: Bool,
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
source: source,
loop: .constant(loop),
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}

/// `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.
///
Expand All @@ -30,14 +58,14 @@ public struct GIFImage: View {
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
public init(
source: GIFSource,
loop: Bool = true,
loop: Binding<Bool> = Binding.constant(true),
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.source = source
self.loop = loop
self._loop = loop
self.placeholder = placeholder
self.errorImage = errorImage
self.frameRate = frameRate
Expand All @@ -48,20 +76,29 @@ public struct GIFImage: View {
Image.loadImage(with: frame ?? placeholder)
.resizable()
.scaledToFit()
.onChange(of: loop, perform: handle(loop:))
.task(id: source, load)
}

private func handle(loop: Bool) {
guard loop else { return }
Task { await load() }
}

@Sendable
private func load() async {
do {
repeat {
for try await imageFrame in try await imageLoader.load(source: source) {
try await update(imageFrame)
}
try await action(source)
} while(self.loop)
} catch {
frame = errorImage ?? placeholder
presentationTask?.cancel()
presentationTask = Task {
do {
repeat {
for try await imageFrame in try await imageLoader.load(source: source) {
try await update(imageFrame)
}
try await action(source)
} while(self.loop)
} catch {
frame = errorImage ?? placeholder
}
}
}

Expand All @@ -87,14 +124,15 @@ struct GIFImage_Previews: PreviewProvider {
static let gifURL = "https://raw.githubusercontent.com/igorcferreira/GIFImage/main/Tests/test.gif"
static let placeholder = RawImage.create(symbol: "photo.circle.fill")!
static let error = RawImage.create(symbol: "xmark.octagon")

static var loop = true

static var previews: some View {
Group {
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error)
.frame(width: 350.0, height: 197.0, alignment: .center)
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error, frameRate: .limited(fps: 5))
.frame(width: 350.0, height: 197.0, alignment: .center)
GIFImage(url: gifURL, loop: false, placeholder: placeholder, errorImage: error, frameRate: .static(fps: 30))
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error, frameRate: .static(fps: 30))
.frame(width: 350.0, height: 197.0, alignment: .center)
}
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/ImageLoaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class ImageLoaderTests: XCTestCase {
let fileManager = MockedFileManager()
let thrownError = URLError(.init(rawValue: 404))

let imageLoader = ImageLoader(session: urlSession, cache: .shared, fileManager: fileManager)
let imageLoader = ImageLoader(session: urlSession, cache: URLCache(memoryCapacity: 0, diskCapacity: 0), fileManager: fileManager)

MockedURLProtocol.register(.failure(thrownError), to: url)

Expand Down

0 comments on commit 062778f

Please sign in to comment.