diff --git a/Source/FetchImage.swift b/Source/FetchImage.swift deleted file mode 100755 index f1b4309..0000000 --- a/Source/FetchImage.swift +++ /dev/null @@ -1,265 +0,0 @@ -// -// The MIT License (MIT) -// -// Copyright (c) 2020 Alexander Grebenyuk (github.com/kean). -// - -import SwiftUI -import Nuke -import FirebaseStorage - -/// Fetch a remote image and progressively load using cached resources first, if -/// available, then displaying a placeholder until fully loaded. -/// -public final class FireFetchImage: ObservableObject, Identifiable { - - // MARK: - Paramaters - - /// The original request. - /// - public private(set) var request: ImageRequest? - - /// Returns the fetched image. - /// - /// - note: In case pipeline has `isProgressiveDecodingEnabled` option enabled - /// and the image being downloaded supports progressive decoding, the `image` - /// might be updated multiple times during the download. - /// - @Published public private(set) var image: PlatformImage? { - didSet { - DispatchQueue.global(qos: .userInitiated).async { - if let uiImage = self.image?.imageWithoutBaseline() { - uiImage.getColors(quality: .high) { uiImageColors in - guard let foundColors = uiImageColors else { return } - Thread.executeOnMain { - self.imageColors = ImageColors(from: foundColors) - } - } - } - } - } - } - - /// Returns an error if the previous attempt to fetch the image failed with an - /// error. Error is cleared out when the download is restarted. - /// - @Published public private(set) var error: Error? - - public struct Progress { - /// The number of bytes that the task has received. - /// - public let completed: Int64 - - /// A best-guess upper bound on the number of bytes the client expects to send. - /// - public let total: Int64 - } - - /// The progress of the image download. - /// - @Published public var progress = Progress(completed: 0, total: 0) - - /// Suggested background, accent, and foreground colors based on the loaded image. - /// - @Published public var imageColors: ImageColors? - - /// Updates the priority of the task, even if the task is already running. - /// - public var priority: ImageRequest.Priority = .normal { - didSet { task?.priority = priority } - } - - public var pipeline: ImagePipeline = .shared - private var task: ImageTask? - - - // MARK: - Lifecycle - - deinit { - cancel() - } - - public init() {} - - public func load(_ url: URL) { - self.load(ImageRequest(url: url)) - } - - /// Initializes the fetch request with a Firebase Storage Reference to an image in - /// any of Nuke's supported formats. The remote URL is then fetched from Firebase - /// and the image is subsequently fetched as well. - /// - /// - parameter regularStorageRef: A `StorageReference` which points to a - /// Firebase Storage file in any of Nuke's supported image formats. - /// - parameter uniqueURL: A caller to request any potentially cached image URLs. - /// Implementing your own URL caching prevents potentially unnecessary roundtrips to - /// your Firebase Storage bucket. - /// - parameter finished: Called when URL loading has completed and fetching can - /// begin. If the caller is `nil`, a fetch operation is queued immediately. - /// - public func load(regularStorageRef: StorageReference, uniqueURL: (() -> URL?)? = nil, finished: ((URL?) -> Void)? = nil) { - func finishOrLoad(_ request: ImageRequest, discoveredURL: URL? = nil) { - if let completionBlock = finished { - completionBlock(discoveredURL) - } - load(request) - } - - func getRegularURL() { - DispatchQueue.global(qos: .userInteractive).async { - regularStorageRef.downloadURL { (discoveredURL, error) in - if let given = discoveredURL { - let newRequest = ImageRequest(url: given) - self.request = newRequest - self.priority = newRequest.priority - - finishOrLoad(newRequest, discoveredURL: given) - } else { - finished?(discoveredURL) - } - } - } - } - - // If provided, query the uniqueURL block for a cached URL. - // If successful, use that parameter instead. - if let uniqueURLBlock = uniqueURL { - if let givenURL = uniqueURLBlock() { - // An existing unique URL where the image may be found or cached. - let newRequest = ImageRequest(url: givenURL) - self.request = newRequest - self.priority = newRequest.priority - finishOrLoad(newRequest, discoveredURL: givenURL) - return // Return early, no need to awaken the Firebeasty - } - } - - getRegularURL() - } - - // MARK: - Fetching - - /// Starts loading the image if not already loaded and the download is not already - /// in progress. - /// - public func load(_ request: ImageRequest) { - _reset() - - // Cancel previous task after starting a new one to make sure that if - // there is an existing task already running we don't cancel it and start - // a new once. - let previousTask = self.task - defer { previousTask?.cancel() } - - self.request = request - - // Try to display the regular image if it is available in memory cache - if let image = pipeline.cache[request] { - Thread.executeOnMain { - self.image = image.image - } - return // Nothing to do - } - - _load(request: request) - } - - private func _load(request: ImageRequest) { - Thread.executeOnMain { - self.progress = Progress(completed: 0, total: 0) - } - - task = pipeline.loadImage( - with: request, - progress: { response, completed, total in - Thread.executeOnMain { - self.progress = Progress(completed: completed, total: total) - } - - if let image = response?.image { - Thread.executeOnMain { - self.image = image // Display progressively decoded image - } - } - }, - completion: { result in - self.didFinishRequest(result: result) - } - ) - - if priority != request.priority { - task?.priority = priority - } - } - - private func didFinishRequest(result: Result) { - defer { - task = nil - } - - switch result { - case let .success(response): - Thread.executeOnMain { - self.image = response.image - } - case let .failure(error): - Thread.executeOnMain { - self.error = error - } - } - } - - - // MARK: - State - - /// Marks the request as being cancelled. Continues to display a downloaded image. - /// - public func cancel() { - task?.cancel() // Guarantees that no more callbacks are will be delivered - task = nil - } - - /// Resets the `FetchImage` instance by cancelling the request and removing all of - /// the state including the loaded image. - /// - public func reset() { - cancel() - _reset() - } - - private func _reset() { - Thread.executeOnMain { - self.image = nil - self.error = nil - self.progress = Progress(completed: 0, total: 0) - } - request = nil - } - -} - -public extension FetchImage { - - var view: SwiftUI.Image? { - #if os(macOS) - return image.map(Image.init(nsImage:)) - #else - return image.map(Image.init(uiImage:)) - #endif - } - -} - -fileprivate extension Thread { - - class func executeOnMain(_ mainBlock: @escaping () -> Void) { - if Thread.isMainThread == true { - mainBlock() - } else { - DispatchQueue.main.async { - mainBlock() - } - } - } - -} diff --git a/Source/FireImage.swift b/Source/FireImage.swift index 0d772ff..f1213f8 100644 --- a/Source/FireImage.swift +++ b/Source/FireImage.swift @@ -13,6 +13,10 @@ import Foundation import Nuke import SwiftUI +public enum FireImageError: Error { + case sourceEmpty +} + /// An observable object that simplifies image loading in SwiftUI. /// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) @@ -100,21 +104,36 @@ public final class FireImage: ObservableObject, Identifiable { private var imageTask: ImageTask? - // publisher support + // MARK: - Publisher Support + private var lastResponse: ImageResponse? private var cancellable: AnyCancellable? + // MARK: - Lifecycle + deinit { cancel() } public init() {} - // MARK: Load (ImageRequestConvertible) + // MARK: Load public typealias CompletedLoad = ((URL?) async -> Void) public typealias UniqueURL = (() async throws -> URL) + /// Initializes the fetch request with a Firebase `StorageReference` to an image in + /// any of Nuke's supported formats. The remote URL is then fetched from Firebase + /// and the image is subsequently fetched as well. + /// + /// - parameter regularStorageRef: A `StorageReference` which points to a + /// Firebase Storage file in any of Nuke's supported image formats. + /// - parameter uniqueURL: A caller to request any potentially cached image URLs. + /// Implementing your own URL caching prevents potentially unnecessary roundtrips to + /// your Firebase Storage bucket. + /// - parameter finished: Called when URL loading has completed and fetching can + /// begin. If the caller is `nil`, a fetch operation is queued immediately. + /// public func load(_ regularStorageRef: StorageReference, uniqueURL: UniqueURL?, finished: CompletedLoad?) async { // If provided, query the uniqueURL block for a cached URL. // If successful, use that parameter instead. @@ -140,9 +159,7 @@ public final class FireImage: ObservableObject, Identifiable { await completionBlock(discoveredURL) } - DispatchQueue.main.async { - self.load(request) - } + await load(request) } private func fetchStandardURL(for regularStorageRef: StorageReference, finish: CompletedLoad?) async { @@ -157,65 +174,13 @@ public final class FireImage: ObservableObject, Identifiable { } } - /// Initializes the fetch request with a Firebase Storage Reference to an image in - /// any of Nuke's supported formats. The remote URL is then fetched from Firebase - /// and the image is subsequently fetched as well. - /// - /// - parameter regularStorageRef: A `StorageReference` which points to a - /// Firebase Storage file in any of Nuke's supported image formats. - /// - parameter uniqueURL: A caller to request any potentially cached image URLs. - /// Implementing your own URL caching prevents potentially unnecessary roundtrips to - /// your Firebase Storage bucket. - /// - parameter finished: Called when URL loading has completed and fetching can - /// begin. If the caller is `nil`, a fetch operation is queued immediately. - /// - public func load(regularStorageRef: StorageReference, uniqueURL: (() -> URL?)? = nil, finished: ((URL?) -> Void)? = nil) { - func finishOrLoad(_ request: ImageRequest, discoveredURL: URL? = nil) { - if let completionBlock = finished { - completionBlock(discoveredURL) - } - load(request) - } - - func getRegularURL() { - DispatchQueue.global(qos: .userInteractive).async { - regularStorageRef.downloadURL { (discoveredURL, error) in - if let given = discoveredURL { - let newRequest = ImageRequest(url: given) - self.priority = newRequest.priority - - finishOrLoad(newRequest, discoveredURL: given) - } else { - finished?(discoveredURL) - } - } - } - } - - // If provided, query the uniqueURL block for a cached URL. - // If successful, use that parameter instead. - if let uniqueURLBlock = uniqueURL { - if let givenURL = uniqueURLBlock() { - // An existing unique URL where the image may be found or cached. - let newRequest = ImageRequest(url: givenURL) - self.priority = newRequest.priority - finishOrLoad(newRequest, discoveredURL: givenURL) - return // Return early, no need to awaken the Firebeasty - } - } - - getRegularURL() - } - /// Loads an image with the given request. /// - public func load(_ request: ImageRequestConvertible?) { - assert(Thread.isMainThread, "Must be called from the main thread") - + @MainActor public func load(_ request: ImageRequestConvertible?) { reset() guard var request = request?.asImageRequest() else { - handle(result: .failure(FetchImageError.sourceEmpty), isSync: true) + handle(result: .failure(FireImageError.sourceEmpty), isSync: true) return } @@ -231,7 +196,7 @@ public final class FireImage: ObservableObject, Identifiable { if image.isPreview { imageContainer = image // Display progressive image } else { - let response = ImageResponse(container: image, cacheType: .memory) + let response = ImageResponse(container: image, request: request, cacheType: .disk) handle(result: .success(response), isSync: true) return }