Skip to content

Commit fefa944

Browse files
committed
Add sources
1 parent f44af8a commit fefa944

File tree

4 files changed

+222
-1
lines changed

4 files changed

+222
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ playground.xcworkspace
4444
#
4545
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
4646
# hence it is not needed unless you have added a package configuration file to your project
47-
# .swiftpm
47+
.swiftpm
4848

4949
.build/
5050

Package.resolved

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// swift-tools-version:5.1
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "FetchImage",
7+
platforms: [
8+
.macOS(.v10_15),
9+
.iOS(.v13),
10+
.tvOS(.v13),
11+
.watchOS(.v5)
12+
],
13+
products: [
14+
.library(name: "FetchImage", targets: ["FetchImage"])
15+
],
16+
dependencies: [
17+
.package(url: "https://github.com/kean/Nuke.git", from: "8.0.0")
18+
],
19+
targets: [
20+
.target(name: "FetchImage", dependencies: ["Nuke"], path: "Source")
21+
]
22+
)

Source/FetchImage.swift

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import SwiftUI
2+
import Nuke
3+
4+
/// - WARNING: This is an API preview. It is not battle-tested yet and might signficantly change in the future.
5+
public final class FetchImage: ObservableObject, Identifiable {
6+
/// The original request.
7+
public let request: ImageRequest
8+
9+
/// The request to be performed if the original request fails with
10+
/// `networkUnavailableReason` `.constrained` (low data mode).
11+
public let lowDataRequest: ImageRequest?
12+
13+
/// Returns the fetched image.
14+
///
15+
/// - note: In case pipeline has `isProgressiveDecodingEnabled` option enabled
16+
/// and the image being downloaded supports progressive decoding, the `image`
17+
/// might be updated multiple times during the download.
18+
@Published public private(set) var image: PlatformImage?
19+
20+
/// Returns an error if the previous attempt to fetch the most recent attempt
21+
/// to load the image failed with an error.
22+
@Published public private(set) var error: Error?
23+
24+
/// Returns `true` if the image is being loaded.
25+
@Published public private(set) var isLoading: Bool = false
26+
27+
public struct Progress {
28+
/// The number of bytes that the task has received.
29+
public let completed: Int64
30+
31+
/// A best-guess upper bound on the number of bytes the client expects to send.
32+
public let total: Int64
33+
}
34+
35+
/// The progress of the image download.
36+
@Published public var progress = Progress(completed: 0, total: 0)
37+
38+
/// Updates the priority of the task, even if the task is already running.
39+
public var priority: ImageRequest.Priority {
40+
didSet { task?.priority = priority }
41+
}
42+
43+
private let pipeline: ImagePipeline
44+
private var task: ImageTask?
45+
private var loadedImageQuality: ImageQuality? = nil
46+
47+
private enum ImageQuality {
48+
case regular, low
49+
}
50+
51+
deinit {
52+
cancel()
53+
}
54+
55+
/// Initializes the fetch request and immediately start loading.
56+
public init(request: ImageRequest, lowDataRequest: ImageRequest? = nil, pipeline: ImagePipeline = .shared) {
57+
self.request = request
58+
self.lowDataRequest = lowDataRequest
59+
self.priority = request.priority
60+
self.pipeline = pipeline
61+
62+
self.fetch()
63+
}
64+
65+
/// Initializes the fetch request and immediately start loading.
66+
public convenience init(url: URL, pipeline: ImagePipeline = .shared) {
67+
self.init(request: ImageRequest(url: url), pipeline: pipeline)
68+
}
69+
70+
/// A convenience initializer that fetches the image with a regular URL with
71+
/// constrained network access disabled, and if the download fails because of
72+
/// the constrained network access, uses a low data URL instead.
73+
public convenience init(regularUrl: URL, lowDataUrl: URL, pipeline: ImagePipeline = .shared) {
74+
var request = URLRequest(url: regularUrl)
75+
request.allowsConstrainedNetworkAccess = false
76+
77+
self.init(request: ImageRequest(urlRequest: request), lowDataRequest: ImageRequest(url: lowDataUrl), pipeline: pipeline)
78+
}
79+
80+
/// Starts loading the image if not already loaded and the download is not
81+
/// already in progress.
82+
///
83+
/// - note: Low Data Mode. If the `lowDataRequest` is provided and the regular
84+
/// request fails because of the constrained network access, the fetcher tries
85+
/// to download the low-quality image. The fetcher always tries to get the high
86+
/// quality image. If the first attempt fails, the next time you call `fetch`,
87+
/// it is going to attempt to fetch the regular quality image again.
88+
public func fetch() {
89+
guard !isLoading, loadedImageQuality != .regular else {
90+
return
91+
}
92+
93+
error = nil
94+
95+
// Try to display the regular image if it is available in memory cache
96+
if let response = pipeline.cachedResponse(for: request) {
97+
(image, loadedImageQuality) = (response.image, .regular)
98+
return // Nothing to do
99+
}
100+
101+
// Try to display the low data image and retry loading the regular image
102+
if let response = lowDataRequest.flatMap(pipeline.cachedResponse(for:)) {
103+
(image, loadedImageQuality) = (response.image, .low)
104+
}
105+
106+
isLoading = true
107+
loadImage(request: request, quality: .regular)
108+
}
109+
110+
private func loadImage(request: ImageRequest, quality: ImageQuality) {
111+
progress = Progress(completed: 0, total: 0)
112+
113+
task = pipeline.loadImage(
114+
with: request,
115+
progress: { [weak self] response, completed, total in
116+
guard let self = self else { return }
117+
118+
self.progress = Progress(completed: completed, total: total)
119+
120+
if let image = response?.image {
121+
self.image = image // Display progressively decoded image
122+
}
123+
},
124+
completion: { [weak self] in
125+
self?.didFinishRequest(result: $0, quality: quality)
126+
}
127+
)
128+
129+
if priority != request.priority {
130+
task?.priority = priority
131+
}
132+
}
133+
134+
private func didFinishRequest(result: Result<ImageResponse, ImagePipeline.Error>, quality: ImageQuality) {
135+
task = nil
136+
137+
switch result {
138+
case let .success(response):
139+
isLoading = false
140+
(image, loadedImageQuality) = (response.image, quality)
141+
case let .failure(error):
142+
// If the regular request fails because of the low data mode,
143+
// use an alternative source.
144+
if quality == .regular, error.isConstrainedNetwork, let request = self.lowDataRequest {
145+
if loadedImageQuality == .low {
146+
isLoading = false // Low-quality image already loaded
147+
} else {
148+
loadImage(request: request, quality: .low)
149+
}
150+
} else {
151+
self.error = error
152+
isLoading = false
153+
}
154+
}
155+
}
156+
157+
/// Marks the request as being cancelled.
158+
public func cancel() {
159+
task?.cancel() // Guarantees that no more callbacks are will be delivered
160+
task = nil
161+
isLoading = false
162+
}
163+
}
164+
165+
private extension ImagePipeline.Error {
166+
var isConstrainedNetwork: Bool {
167+
if case let .dataLoadingFailed(error) = self,
168+
(error as? URLError)?.networkUnavailableReason == .constrained {
169+
return true
170+
}
171+
return false
172+
}
173+
}
174+
175+
public extension FetchImage {
176+
var view: SwiftUI.Image? {
177+
#if os(macOS)
178+
return image.map(Image.init(nsImage:))
179+
#else
180+
return image.map(Image.init(uiImage:))
181+
#endif
182+
}
183+
}

0 commit comments

Comments
 (0)