Skip to content

Commit 24bae83

Browse files
committed
New FireImage
1 parent 6a053dd commit 24bae83

File tree

2 files changed

+290
-0
lines changed

2 files changed

+290
-0
lines changed

.DS_Store

0 Bytes
Binary file not shown.

Source/FireImage.swift

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
//
2+
// FireImage.swift
3+
//
4+
//
5+
// Created by Sam Spencer on 7/11/21.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
import Combine
11+
import Nuke
12+
import FirebaseStorage
13+
14+
/// An observable object that simplifies image loading in SwiftUI.
15+
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
16+
public final class FireImage: ObservableObject, Identifiable {
17+
18+
/// Returns the current fetch result.
19+
@Published public private(set) var result: Result<ImageResponse, Error>?
20+
21+
/// Returns the fetched image.
22+
///
23+
/// - note: In case pipeline has `isProgressiveDecodingEnabled` option enabled
24+
/// and the image being downloaded supports progressive decoding, the `image`
25+
/// might be updated multiple times during the download.
26+
public var image: PlatformImage? { imageContainer?.image }
27+
28+
/// Returns the fetched image.
29+
///
30+
/// - note: In case pipeline has `isProgressiveDecodingEnabled` option enabled
31+
/// and the image being downloaded supports progressive decoding, the `image`
32+
/// might be updated multiple times during the download.
33+
@Published public private(set) var imageContainer: ImageContainer?
34+
35+
/// Returns `true` if the image is being loaded.
36+
@Published public private(set) var isLoading: Bool = false
37+
38+
/// Animations to be used when displaying the loaded images. By default, `nil`.
39+
///
40+
/// - note: Animation isn't used when image is available in memory cache.
41+
public var animation: Animation?
42+
43+
/// The download progress.
44+
public struct Progress: Equatable {
45+
/// The number of bytes that the task has received.
46+
public let completed: Int64
47+
48+
/// A best-guess upper bound on the number of bytes the client expects to send.
49+
public let total: Int64
50+
}
51+
52+
/// The progress of the image download.
53+
@Published public private(set) var progress = Progress(completed: 0, total: 0)
54+
55+
/// Updates the priority of the task, even if the task is already running.
56+
/// `nil` by default
57+
public var priority: ImageRequest.Priority? {
58+
didSet { priority.map { imageTask?.priority = $0 } }
59+
}
60+
61+
/// Gets called when the request is started.
62+
public var onStart: ((_ task: ImageTask) -> Void)?
63+
64+
/// Gets called when the request progress is updated.
65+
public var onProgress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?
66+
67+
/// Gets called when the requests finished successfully.
68+
public var onSuccess: ((_ response: ImageResponse) -> Void)?
69+
70+
/// Gets called when the requests fails.
71+
public var onFailure: ((_ response: Error) -> Void)?
72+
73+
/// Gets called when the request is completed.
74+
public var onCompletion: ((_ result: Result<ImageResponse, Error>) -> Void)?
75+
76+
public var pipeline: ImagePipeline = .shared
77+
78+
/// Image processors to be applied unless the processors are provided in the request.
79+
/// `nil` by default.
80+
public var processors: [ImageProcessing]?
81+
82+
private var imageTask: ImageTask?
83+
84+
// publisher support
85+
private var lastResponse: ImageResponse?
86+
private var cancellable: AnyCancellable?
87+
88+
deinit {
89+
cancel()
90+
}
91+
92+
public init() {}
93+
94+
// MARK: Load (ImageRequestConvertible)
95+
96+
/// Initializes the fetch request with a Firebase Storage Reference to an image in
97+
/// any of Nuke's supported formats. The remote URL is then fetched from Firebase
98+
/// and the image is subsequently fetched as well.
99+
///
100+
/// - parameter regularStorageRef: A `StorageReference` which points to a
101+
/// Firebase Storage file in any of Nuke's supported image formats.
102+
/// - parameter uniqueURL: A caller to request any potentially cached image URLs.
103+
/// Implementing your own URL caching prevents potentially unnecessary roundtrips to
104+
/// your Firebase Storage bucket.
105+
/// - parameter finished: Called when URL loading has completed and fetching can
106+
/// begin. If the caller is `nil`, a fetch operation is queued immediately.
107+
///
108+
public func load(regularStorageRef: StorageReference, uniqueURL: (() -> URL?)? = nil, finished: ((URL?) -> Void)? = nil) {
109+
func finishOrLoad(_ request: ImageRequest, discoveredURL: URL? = nil) {
110+
if let completionBlock = finished {
111+
completionBlock(discoveredURL)
112+
}
113+
load(request)
114+
}
115+
116+
func getRegularURL() {
117+
DispatchQueue.global(qos: .userInteractive).async {
118+
regularStorageRef.downloadURL { (discoveredURL, error) in
119+
if let given = discoveredURL {
120+
let newRequest = ImageRequest(url: given)
121+
self.priority = newRequest.priority
122+
123+
finishOrLoad(newRequest, discoveredURL: given)
124+
} else {
125+
finished?(discoveredURL)
126+
}
127+
}
128+
}
129+
}
130+
131+
// If provided, query the uniqueURL block for a cached URL.
132+
// If successful, use that parameter instead.
133+
if let uniqueURLBlock = uniqueURL {
134+
if let givenURL = uniqueURLBlock() {
135+
// An existing unique URL where the image may be found or cached.
136+
let newRequest = ImageRequest(url: givenURL)
137+
self.priority = newRequest.priority
138+
finishOrLoad(newRequest, discoveredURL: givenURL)
139+
return // Return early, no need to awaken the Firebeasty
140+
}
141+
}
142+
143+
getRegularURL()
144+
}
145+
146+
/// Loads an image with the given request.
147+
public func load(_ request: ImageRequestConvertible?) {
148+
assert(Thread.isMainThread, "Must be called from the main thread")
149+
150+
reset()
151+
152+
guard var request = request?.asImageRequest() else {
153+
handle(result: .failure(FetchImageError.sourceEmpty), isSync: true)
154+
return
155+
}
156+
157+
if let processors = self.processors, !processors.isEmpty && request.processors.isEmpty {
158+
request.processors = processors
159+
}
160+
if let priority = self.priority {
161+
request.priority = priority
162+
}
163+
164+
// Quick synchronous memory cache lookup
165+
if let image = pipeline.cache[request] {
166+
if image.isPreview {
167+
imageContainer = image // Display progressive image
168+
} else {
169+
let response = ImageResponse(container: image, cacheType: .memory)
170+
handle(result: .success(response), isSync: true)
171+
return
172+
}
173+
}
174+
175+
isLoading = true
176+
progress = Progress(completed: 0, total: 0)
177+
178+
let task = pipeline.loadImage(
179+
with: request,
180+
progress: { [weak self] response, completed, total in
181+
guard let self = self else { return }
182+
self.progress = Progress(completed: completed, total: total)
183+
if let response = response {
184+
withAnimation(self.animation) {
185+
self.handle(preview: response)
186+
}
187+
}
188+
self.onProgress?(response, completed, total)
189+
},
190+
completion: { [weak self] result in
191+
guard let self = self else { return }
192+
withAnimation(self.animation) {
193+
self.handle(result: result.mapError { $0 }, isSync: false)
194+
}
195+
}
196+
)
197+
imageTask = task
198+
onStart?(task)
199+
}
200+
201+
private func handle(preview: ImageResponse) {
202+
// Display progressively decoded image
203+
self.imageContainer = preview.container
204+
}
205+
206+
private func handle(result: Result<ImageResponse, Error>, isSync: Bool) {
207+
isLoading = false
208+
209+
if case .success(let response) = result {
210+
self.imageContainer = response.container
211+
}
212+
self.result = result
213+
214+
imageTask = nil
215+
switch result {
216+
case .success(let response): onSuccess?(response)
217+
case .failure(let error): onFailure?(error)
218+
}
219+
onCompletion?(result)
220+
}
221+
222+
// MARK: Load (Publisher)
223+
224+
/// Loads an image with the given publisher.
225+
///
226+
/// - warning: Some `FetchImage` features, such as progress reporting and
227+
/// dynamically changing the request priority, are not available when
228+
/// working with a publisher.
229+
public func load<P: Publisher>(_ publisher: P) where P.Output == ImageResponse {
230+
reset()
231+
232+
// Not using `first()` because it should support progressive decoding
233+
isLoading = true
234+
cancellable = publisher.sink(receiveCompletion: { [weak self] completion in
235+
guard let self = self else { return }
236+
self.isLoading = false
237+
switch completion {
238+
case .finished:
239+
if let response = self.lastResponse {
240+
self.result = .success(response)
241+
} // else was cancelled, do nothing
242+
case .failure(let error):
243+
self.result = .failure(error)
244+
}
245+
}, receiveValue: { [weak self] response in
246+
guard let self = self else { return }
247+
self.lastResponse = response
248+
self.imageContainer = response.container
249+
})
250+
}
251+
252+
// MARK: Cancel
253+
254+
/// Marks the request as being cancelled. Continues to display a downloaded
255+
/// image.
256+
public func cancel() {
257+
// pipeline-based
258+
imageTask?.cancel() // Guarantees that no more callbacks are will be delivered
259+
imageTask = nil
260+
261+
// publisher-based
262+
cancellable = nil
263+
264+
// common
265+
if isLoading { isLoading = false }
266+
}
267+
268+
/// Resets the `FetchImage` instance by cancelling the request and removing
269+
/// all of the state including the loaded image.
270+
public func reset() {
271+
cancel()
272+
273+
// Avoid publishing unchanged values
274+
if isLoading { isLoading = false }
275+
if imageContainer != nil { imageContainer = nil }
276+
if result != nil { result = nil }
277+
lastResponse = nil // publisher-only
278+
if progress != Progress(completed: 0, total: 0) { progress = Progress(completed: 0, total: 0) }
279+
}
280+
281+
// MARK: View
282+
283+
public var view: SwiftUI.Image? {
284+
#if os(macOS)
285+
return image.map(Image.init(nsImage:))
286+
#else
287+
return image.map(Image.init(uiImage:))
288+
#endif
289+
}
290+
}

0 commit comments

Comments
 (0)