Skip to content

Commit

Permalink
Auto-cancel requests if needed
Browse files Browse the repository at this point in the history
  • Loading branch information
Sam-Spencer committed Mar 4, 2021
1 parent 875cf2b commit 1d43d72
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 13 deletions.
46 changes: 35 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,64 @@
> **Note**. This is an API preview. It is not battle-tested yet, and might change in the future.
## Usage

Here is an example of using `FetchImage` in a custom SwiftUI view.

```swift
public struct ImageView: View {
@ObservedObject var image: FetchImage
let url: URL

@StateObject private var image = FetchImage()

public var body: some View {
ZStack {
Rectangle().fill(Color.gray)
image.view?
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
}

// Cancel and restart the request during scrolling
// If the view is still on screen, use `cancel()` instead of `reset()`.
.onAppear(perform: image.fetch)
.onAppear {
// Ensure that synchronous cache lookup doesn't trigger animations
withoutAnimation {
image.load(url)
}
}
.onDisappear(perform: image.reset)

// (Optional) Animate image appearance
.animation(.default)
}
}

struct ImageView_Previews: PreviewProvider {
static var previews: some View {
let url = URL(string: "https://cloud.githubusercontent.com/assets/1567433/9781817/ecb16e82-57a0-11e5-9b43-6b4f52659997.jpg")!
return ImageView(image: FetchImage(url: url))
.frame(width: 80, height: 80)
.clipped()
private func withoutAnimation(_ closure: () -> Void) {
var transaction = Transaction(animation: nil)
transaction.disablesAnimations = true
withTransaction(transaction, closure)
}
```
> For iOS 13, use `@ObservedObject`.
> WARNING: `@ObservedObject` does own the instance, you need to maintain the strong references to the `FetchImage` instances somewhere else.
### Lists
`FetchImage` may also be used in a `List`:

```swift
struct DetailsView: View {
@State var refresh: Bool = false

var body: some View {
List(imageUrls, id: \.self) {
ImageView(url: $0)
.frame(height: 200)
}
}
}
```

### Priority
`FetchImage` gives you full control over how to manage the download and how to display the image. For example, one thing that you could do is to replace `onAppear` and `onDisappear` hooks to lower the priority of the requests instead of cancelling them. This might be useful if you want to continue loading and caching the images even if the user leaves the screen, but you still want the images the are currently on screen to be downloaded first.

```swift
Expand All @@ -56,6 +79,7 @@ struct ImageView_Previews: PreviewProvider {
}
```

### Firebase
You may also initialize a `FetchImage` using a Firestore `StorageReference`. These references can be easily created synchronously, but require an asynchronous call in order generate URLs for fetching the requested content. Unfortunately, this makes image loading in SwiftUI rather difficult. Using `Nuke` and `Firebase` together simplifies the whole process quite a bit:

```swift
Expand All @@ -69,7 +93,7 @@ public var body: some View {

## Overview

`FetchImage` is an observable object (`ObservableObject`) that allows you to manage the download of a single image and observe the results of the download. All of the changes to the download state are published using properties marked with `@Published` property wrapper.
`FetchImage` is a state object, `StateObject` (use `ObservableObject` on iOS 13) that allows you to manage the download of a single image and observe the results of the download. All of the changes to the download state are published using properties marked with `@Published` property wrapper.

```swift
public final class FetchImage: ObservableObject, Identifiable {
Expand Down Expand Up @@ -144,7 +168,7 @@ FetchImage(regularUrl: highQualityUrl, lowDataUrl: lowQualityUrl)

| Nuke | Swift | Xcode | Platforms |
|---------------|-----------------|-----------------|---------------------------------------------------|
| FetchImage | Swift 5.1 | Xcode 11.3 | iOS 13.0 / watchOS 6.0 / macOS 10.15 / tvOS 13.0 |
| FetchImage | Swift 5.3 | Xcode 12 | iOS 13.0 / watchOS 6.0 / macOS 10.15 / tvOS 13.0 |

# License

Expand Down
31 changes: 29 additions & 2 deletions Source/FetchImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,35 @@ public final class FetchImage: ObservableObject, Identifiable {
// MARK: - Paramaters

/// The original request.
public private(set) var request: ImageRequest?
public private(set) var request: ImageRequest? {
didSet {
assert(Thread.isMainThread, "Only modify the request from the main thread.")
if currentlyLoadingImageQuality == .regular {
cancel()
}
guard let newRequest = request else {
if loadedImageQuality == .regular {
image = nil
}
return
}
priority = newRequest.priority
}
}

/// The request to be performed if the original request fails with
/// `networkUnavailableReason` `.constrained` (low data mode).
public private(set) var lowDataRequest: ImageRequest?
public private(set) var lowDataRequest: ImageRequest? {
didSet {
assert(Thread.isMainThread, "Only modify the request from the main thread.")
if currentlyLoadingImageQuality == .low {
cancel()
}
if lowDataRequest == nil && loadedImageQuality == .low {
image = nil
}
}
}

/// Returns the fetched image.
///
Expand Down Expand Up @@ -56,6 +80,7 @@ public final class FetchImage: ObservableObject, Identifiable {
public var pipeline: ImagePipeline = .shared
private var task: ImageTask?
private var loadedImageQuality: ImageQuality?
private var currentlyLoadingImageQuality: ImageQuality? = nil

private enum ImageQuality {
case regular
Expand Down Expand Up @@ -200,6 +225,7 @@ public final class FetchImage: ObservableObject, Identifiable {

private func load(request: ImageRequest, quality: ImageQuality) {
progress = Progress(completed: 0, total: 0)
currentlyLoadingImageQuality = quality

task = pipeline.loadImage(
with: request,
Expand All @@ -224,6 +250,7 @@ public final class FetchImage: ObservableObject, Identifiable {

private func didFinishRequest(result: Result<ImageResponse, ImagePipeline.Error>, quality: ImageQuality) {
task = nil
currentlyLoadingImageQuality = nil

switch result {
case let .success(response):
Expand Down

0 comments on commit 1d43d72

Please sign in to comment.