Skip to content

Commit

Permalink
Merge pull request #2150 from onevcat/fix/async-request-modifier
Browse files Browse the repository at this point in the history
Adopt async request modifier
  • Loading branch information
onevcat authored Oct 15, 2023
2 parents 402fe7d + 5b5db5a commit 89bd8e8
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 25 deletions.
19 changes: 15 additions & 4 deletions Sources/Networking/ImageDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ open class ImageDownloader {
done: @escaping ((Result<DownloadingContext, KingfisherError>) -> Void)
)
{
func checkRequestAndDone(r: URLRequest) {
@Sendable func checkRequestAndDone(r: URLRequest) {

// There is a possibility that request modifier changed the url to `nil` or empty.
// In this case, throw an error.
Expand All @@ -269,13 +269,24 @@ open class ImageDownloader {

if let requestModifier = options.requestModifier {
// Modifies request before sending.
requestModifier.modified(for: request) { result in
guard let finalRequest = result else {
// FIXME: A temporary solution for keep the sync `ImageDownloadRequestModifier` behavior as before.
// We should be able to combine two cases once the full async support can be introduced to Kingfisher.
if let m = requestModifier as? ImageDownloadRequestModifier {
guard let result = m.modified(for: request) else {
done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
return
}
checkRequestAndDone(r: finalRequest)
checkRequestAndDone(r: result)
} else {
Task { [request] in
guard let result = await requestModifier.modified(for: request) else {
done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
return
}
checkRequestAndDone(r: result)
}
}

} else {
checkRequestAndDone(r: request)
}
Expand Down
55 changes: 38 additions & 17 deletions Sources/Networking/RequestModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,52 @@

import Foundation

/// Represents and wraps a method for modifying request before an image download request starts in an asynchronous way.
/// Represents and wraps a method for modifying a request before an image download request starts asynchronously.
///
/// Usually, you pass an ``AsyncImageDownloadRequestModifier`` instance as the associated value of
/// ``KingfisherOptionsInfoItem/requestModifier`` and use it as the `options` parameter in related methods.
///
/// For example, the code below defines a modifier to add a header field and its value to the request.
///
/// ```swift
/// class HeaderFieldModifier: AsyncImageDownloadRequestModifier {
/// var onDownloadTaskStarted: ((DownloadTask?) -> Void)? = nil
/// func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) {
/// var r = request
/// r.setValue("value", forHTTPHeaderField: "key")
/// reportModified(r)
/// }
/// }
///
/// imageView.kf.setImage(with: url, options: [.requestModifier(HeaderFieldModifier())])
/// ```
///
public protocol AsyncImageDownloadRequestModifier {

/// This method will be called just before the `request` being sent.
/// This is the last chance you can modify the image download request. You can modify the request for some
/// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping.
/// When you have done with the modification, call the `reportModified` block with the modified request and the data
/// download will happen with this request.
/// This method will be called just before the `request` is sent.
///
/// Usually, you pass an `AsyncImageDownloadRequestModifier` as the associated value of
/// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods.
/// This is the last chance to modify the image download request. You can modify the request for some customizing
/// purposes, such as adding an auth token to the header, performing basic HTTP auth, or something like URL mapping.
///
/// After making the modification, call the `reportModified` block with the modified request, and the data
/// will be downloaded with this modified request.
///
/// If you do nothing with the input `request` and return it as is, a downloading process will start with it.
/// > If you do nothing with the input `request` and return it as-is, the download process will start with it as the
/// modifier doesn't exist.
///
/// - Parameters:
/// - request: The input request contains necessary information like `url`. This request is generated
/// according to your resource url as a GET request.
/// - reportModified: The callback block you need to call after the asynchronous modifying done.
///
func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void)
/// - request: The input request contains necessary information like `url`. This request is generated
/// according to your resource URL as a GET request.
/// - reportModified: The callback block you need to call after the asynchronous modification is done.
func modified(for request: URLRequest) async -> URLRequest?

/// A block will be called when the download task started.
/// A block that will be called when the download task starts.
///
/// If an ``AsyncImageDownloadRequestModifier`` and asynchronous modification occur before the download, the
/// related download method will not return a valid ``DownloadTask`` value. Instead, you can get one from this
/// method.
///
/// If an `AsyncImageDownloadRequestModifier` and the asynchronous modification happens before the download, the
/// related download method will not return a valid `DownloadTask` value. Instead, you can get one from this method.
/// User the ``DownloadTask`` value to track the task, or cancel it when you need to.
var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { get }
}

Expand Down
8 changes: 4 additions & 4 deletions Tests/KingfisherTests/ImageDownloaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -710,11 +710,11 @@ class AsyncURLModifier: AsyncImageDownloadRequestModifier {
var url: URL? = nil
var onDownloadTaskStarted: ((DownloadTask?) -> Void)?

func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) {
func modified(for request: URLRequest) async -> URLRequest? {
var r = request
r.url = url
DispatchQueue.main.async {
reportModified(r)
}
// Simulate an async action
try? await Task.sleep(nanoseconds: 1_000_000)
return r
}
}

0 comments on commit 89bd8e8

Please sign in to comment.