This commit is contained in:
DDIsFriend
2023-08-24 16:46:39 +08:00
parent 887a468768
commit 1a0943017a
139 changed files with 24162 additions and 13640 deletions

View File

@@ -0,0 +1,94 @@
//
// AuthenticationChallengeResponsable.swift
// Kingfisher
//
// Created by Wei Wang on 2018/10/11.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
@available(*, deprecated, message: "Typo. Use `AuthenticationChallengeResponsible` instead", renamed: "AuthenticationChallengeResponsible")
public typealias AuthenticationChallengeResponsable = AuthenticationChallengeResponsible
/// Protocol indicates that an authentication challenge could be handled.
public protocol AuthenticationChallengeResponsible: AnyObject {
/// Called when a session level authentication challenge is received.
/// This method provide a chance to handle and response to the authentication
/// challenge before downloading could start.
///
/// - Parameters:
/// - downloader: The downloader which receives this challenge.
/// - challenge: An object that contains the request for authentication.
/// - completionHandler: A handler that your delegate method must call.
///
/// - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`.
/// Please refer to the document of it in `URLSessionDelegate`.
func downloader(
_ downloader: ImageDownloader,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
/// Called when a task level authentication challenge is received.
/// This method provide a chance to handle and response to the authentication
/// challenge before downloading could start.
///
/// - Parameters:
/// - downloader: The downloader which receives this challenge.
/// - task: The task whose request requires authentication.
/// - challenge: An object that contains the request for authentication.
/// - completionHandler: A handler that your delegate method must call.
func downloader(
_ downloader: ImageDownloader,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
}
extension AuthenticationChallengeResponsible {
public func downloader(
_ downloader: ImageDownloader,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
completionHandler(.useCredential, credential)
return
}
}
completionHandler(.performDefaultHandling, nil)
}
public func downloader(
_ downloader: ImageDownloader,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
completionHandler(.performDefaultHandling, nil)
}
}

View File

@@ -0,0 +1,74 @@
//
// ImageDataProcessor.swift
// Kingfisher
//
// Created by Wei Wang on 2018/10/11.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
private let sharedProcessingQueue: CallbackQueue =
.dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process"))
// Handles image processing work on an own process queue.
class ImageDataProcessor {
let data: Data
let callbacks: [SessionDataTask.TaskCallback]
let queue: CallbackQueue
// Note: We have an optimization choice there, to reduce queue dispatch by checking callback
// queue settings in each option...
let onImageProcessed = Delegate<(Result<KFCrossPlatformImage, KingfisherError>, SessionDataTask.TaskCallback), Void>()
init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) {
self.data = data
self.callbacks = callbacks
self.queue = processingQueue ?? sharedProcessingQueue
}
func process() {
queue.execute(doProcess)
}
private func doProcess() {
var processedImages = [String: KFCrossPlatformImage]()
for callback in callbacks {
let processor = callback.options.processor
var image = processedImages[processor.identifier]
if image == nil {
image = processor.process(item: .data(data), options: callback.options)
processedImages[processor.identifier] = image
}
let result: Result<KFCrossPlatformImage, KingfisherError>
if let image = image {
let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image
result = .success(finalImage)
} else {
let error = KingfisherError.processorError(
reason: .processingFailed(processor: processor, item: .data(data)))
result = .failure(error)
}
onImageProcessed.call((result, callback))
}
}
}

View File

@@ -0,0 +1,502 @@
//
// ImageDownloader.swift
// Kingfisher
//
// Created by Wei Wang on 15/4/6.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if os(macOS)
import AppKit
#else
import UIKit
#endif
typealias DownloadResult = Result<ImageLoadingResult, KingfisherError>
/// Represents a success result of an image downloading progress.
public struct ImageLoadingResult {
/// The downloaded image.
public let image: KFCrossPlatformImage
/// Original URL of the image request.
public let url: URL?
/// The raw data received from downloader.
public let originalData: Data
/// Creates an `ImageDownloadResult`
///
/// - parameter image: Image of the download result
/// - parameter url: URL from where the image was downloaded from
/// - parameter originalData: The image's binary data
public init(image: KFCrossPlatformImage, url: URL? = nil, originalData: Data) {
self.image = image
self.url = url
self.originalData = originalData
}
}
/// Represents a task of an image downloading process.
public struct DownloadTask {
/// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer
/// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task
/// for the same URL resource at the same time.
///
/// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through.
/// You can use them to identify the cancelled task.
public let sessionTask: SessionDataTask
/// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled.
/// To cancel a `DownloadTask`, use `cancel` instead.
public let cancelToken: SessionDataTask.CancelToken
/// Cancel this task if it is running. It will do nothing if this task is not running.
///
/// - Note:
/// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being
/// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created
/// and returned when you call related methods, but it will share the session downloading task with a previous task.
/// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask`
/// does not affect other `DownloadTask`s.
///
/// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel
/// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`.
public func cancel() {
sessionTask.cancel(token: cancelToken)
}
}
extension DownloadTask {
enum WrappedTask {
case download(DownloadTask)
case dataProviding
func cancel() {
switch self {
case .download(let task): task.cancel()
case .dataProviding: break
}
}
var value: DownloadTask? {
switch self {
case .download(let task): return task
case .dataProviding: return nil
}
}
}
}
/// Represents a downloading manager for requesting the image with a URL from server.
open class ImageDownloader {
// MARK: Singleton
/// The default downloader.
public static let `default` = ImageDownloader(name: "default")
// MARK: Public Properties
/// The duration before the downloading is timeout. Default is 15 seconds.
open var downloadTimeout: TimeInterval = 15.0
/// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this
/// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't
/// specify the `authenticationChallengeResponder`.
///
/// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of
/// `authenticationChallengeResponder` will be used instead.
open var trustedHosts: Set<String>?
/// Use this to set supply a configuration for the downloader. By default,
/// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
///
/// You could change the configuration before a downloading task starts.
/// A configuration without persistent storage for caches is requested for downloader working correctly.
open var sessionConfiguration = URLSessionConfiguration.ephemeral {
didSet {
session.invalidateAndCancel()
session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
}
}
open var sessionDelegate: SessionDelegate {
didSet {
session.invalidateAndCancel()
session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
setupSessionHandler()
}
}
/// Whether the download requests should use pipeline or not. Default is false.
open var requestsUsePipelining = false
/// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
open weak var delegate: ImageDownloaderDelegate?
/// A responder for authentication challenge.
/// Downloader will forward the received authentication challenge for the downloading session to this responder.
open weak var authenticationChallengeResponder: AuthenticationChallengeResponsible?
private let name: String
private var session: URLSession
// MARK: Initializers
/// Creates a downloader with name.
///
/// - Parameter name: The name for the downloader. It should not be empty.
public init(name: String) {
if name.isEmpty {
fatalError("[Kingfisher] You should specify a name for the downloader. "
+ "A downloader with empty name is not permitted.")
}
self.name = name
sessionDelegate = SessionDelegate()
session = URLSession(
configuration: sessionConfiguration,
delegate: sessionDelegate,
delegateQueue: nil)
authenticationChallengeResponder = self
setupSessionHandler()
}
deinit { session.invalidateAndCancel() }
private func setupSessionHandler() {
sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in
self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2)
}
sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in
self.authenticationChallengeResponder?.downloader(
self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3)
}
sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in
return (self.delegate ?? self).isValidStatusCode(code, for: self)
}
sessionDelegate.onResponseReceived.delegate(on: self) { (self, invoke) in
(self.delegate ?? self).imageDownloader(self, didReceive: invoke.0, completionHandler: invoke.1)
}
sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in
let (url, result) = value
do {
let value = try result.get()
self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil)
} catch {
self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error)
}
}
sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task)
}
}
// Wraps `completionHandler` to `onCompleted` respectively.
private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate<DownloadResult, Void>? {
return completionHandler.map { block -> Delegate<DownloadResult, Void> in
let delegate = Delegate<Result<ImageLoadingResult, KingfisherError>, Void>()
delegate.delegate(on: self) { (self, callback) in
block(callback)
}
return delegate
}
}
private func createTaskCallback(
_ completionHandler: ((DownloadResult) -> Void)?,
options: KingfisherParsedOptionsInfo
) -> SessionDataTask.TaskCallback
{
return SessionDataTask.TaskCallback(
onCompleted: createCompletionCallBack(completionHandler),
options: options
)
}
private func createDownloadContext(
with url: URL,
options: KingfisherParsedOptionsInfo,
done: @escaping ((Result<DownloadingContext, KingfisherError>) -> Void)
)
{
func checkRequestAndDone(r: URLRequest) {
// There is a possibility that request modifier changed the url to `nil` or empty.
// In this case, throw an error.
guard let url = r.url, !url.absoluteString.isEmpty else {
done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r))))
return
}
done(.success(DownloadingContext(url: url, request: r, options: options)))
}
// Creates default request.
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
request.httpShouldUsePipelining = requestsUsePipelining
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil {
request.allowsConstrainedNetworkAccess = false
}
if let requestModifier = options.requestModifier {
// Modifies request before sending.
requestModifier.modified(for: request) { result in
guard let finalRequest = result else {
done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
return
}
checkRequestAndDone(r: finalRequest)
}
} else {
checkRequestAndDone(r: request)
}
}
private func addDownloadTask(
context: DownloadingContext,
callback: SessionDataTask.TaskCallback
) -> DownloadTask
{
// Ready to start download. Add it to session task manager (`sessionHandler`)
let downloadTask: DownloadTask
if let existingTask = sessionDelegate.task(for: context.url) {
downloadTask = sessionDelegate.append(existingTask, callback: callback)
} else {
let sessionDataTask = session.dataTask(with: context.request)
sessionDataTask.priority = context.options.downloadPriority
downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback)
}
return downloadTask
}
private func reportWillDownloadImage(url: URL, request: URLRequest) {
delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
}
private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) {
var response: URLResponse?
var err: Error?
do {
response = try result.get().1
} catch {
err = error
}
self.delegate?.imageDownloader(
self,
didFinishDownloadingImageForURL: url,
with: response,
error: err
)
}
private func reportDidProcessImage(
result: Result<KFCrossPlatformImage, KingfisherError>, url: URL, response: URLResponse?
)
{
if let image = try? result.get() {
self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
}
}
private func startDownloadTask(
context: DownloadingContext,
callback: SessionDataTask.TaskCallback
) -> DownloadTask
{
let downloadTask = addDownloadTask(context: context, callback: callback)
let sessionTask = downloadTask.sessionTask
guard !sessionTask.started else {
return downloadTask
}
sessionTask.onTaskDone.delegate(on: self) { (self, done) in
// Underlying downloading finishes.
// result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
let (result, callbacks) = done
// Before processing the downloaded data.
self.reportDidDownloadImageData(result: result, url: context.url)
switch result {
// Download finished. Now process the data to an image.
case .success(let (data, response)):
let processor = ImageDataProcessor(
data: data, callbacks: callbacks, processingQueue: context.options.processingQueue
)
processor.onImageProcessed.delegate(on: self) { (self, done) in
// `onImageProcessed` will be called for `callbacks.count` times, with each
// `SessionDataTask.TaskCallback` as the input parameter.
// result: Result<Image>, callback: SessionDataTask.TaskCallback
let (result, callback) = done
self.reportDidProcessImage(result: result, url: context.url, response: response)
let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) }
let queue = callback.options.callbackQueue
queue.execute { callback.onCompleted?.call(imageResult) }
}
processor.process()
case .failure(let error):
callbacks.forEach { callback in
let queue = callback.options.callbackQueue
queue.execute { callback.onCompleted?.call(.failure(error)) }
}
}
}
reportWillDownloadImage(url: context.url, request: context.request)
sessionTask.resume()
return downloadTask
}
// MARK: Downloading Task
/// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super.
///
/// - Parameters:
/// - url: Target URL.
/// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
/// - completionHandler: Called when the download progress finishes. This block will be called in the queue
/// defined in `.callbackQueue` in `options` parameter.
/// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
@discardableResult
open func downloadImage(
with url: URL,
options: KingfisherParsedOptionsInfo,
completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
{
var downloadTask: DownloadTask?
createDownloadContext(with: url, options: options) { result in
switch result {
case .success(let context):
// `downloadTask` will be set if the downloading started immediately. This is the case when no request
// modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an
// `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil`
// and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted`
// callback.
downloadTask = self.startDownloadTask(
context: context,
callback: self.createTaskCallback(completionHandler, options: options)
)
if let modifier = options.requestModifier {
modifier.onDownloadTaskStarted?(downloadTask)
}
case .failure(let error):
options.callbackQueue.execute {
completionHandler?(.failure(error))
}
}
}
return downloadTask
}
/// Downloads an image with a URL and option.
///
/// - Parameters:
/// - url: Target URL.
/// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
/// - progressBlock: Called when the download progress updated. This block will be always be called in main queue.
/// - completionHandler: Called when the download progress finishes. This block will be called in the queue
/// defined in `.callbackQueue` in `options` parameter.
/// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
@discardableResult
open func downloadImage(
with url: URL,
options: KingfisherOptionsInfo? = nil,
progressBlock: DownloadProgressBlock? = nil,
completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
{
var info = KingfisherParsedOptionsInfo(options)
if let block = progressBlock {
info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
}
return downloadImage(
with: url,
options: info,
completionHandler: completionHandler)
}
/// Downloads an image with a URL and option.
///
/// - Parameters:
/// - url: Target URL.
/// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
/// - completionHandler: Called when the download progress finishes. This block will be called in the queue
/// defined in `.callbackQueue` in `options` parameter.
/// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
@discardableResult
open func downloadImage(
with url: URL,
options: KingfisherOptionsInfo? = nil,
completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
{
downloadImage(
with: url,
options: KingfisherParsedOptionsInfo(options),
completionHandler: completionHandler
)
}
}
// MARK: Cancelling Task
extension ImageDownloader {
/// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers
/// for all not-yet-finished downloading tasks.
///
/// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask`
/// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url,
/// use `ImageDownloader.cancel(url:)`.
public func cancelAll() {
sessionDelegate.cancelAll()
}
/// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for
/// all not-yet-finished downloading tasks for the URL.
///
/// - Parameter url: The URL which you want to cancel downloading.
public func cancel(url: URL) {
sessionDelegate.cancel(url: url)
}
}
// Use the default implementation from extension of `AuthenticationChallengeResponsible`.
extension ImageDownloader: AuthenticationChallengeResponsible {}
// Use the default implementation from extension of `ImageDownloaderDelegate`.
extension ImageDownloader: ImageDownloaderDelegate {}
extension ImageDownloader {
struct DownloadingContext {
let url: URL
let request: URLRequest
let options: KingfisherParsedOptionsInfo
}
}

View File

@@ -0,0 +1,184 @@
//
// ImageDownloaderDelegate.swift
// Kingfisher
//
// Created by Wei Wang on 2018/10/11.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// Protocol of `ImageDownloader`. This protocol provides a set of methods which are related to image downloader
/// working stages and rules.
public protocol ImageDownloaderDelegate: AnyObject {
/// Called when the `ImageDownloader` object will start downloading an image from a specified URL.
///
/// - Parameters:
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
/// - url: URL of the starting request.
/// - request: The request object for the download process.
///
func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?)
/// Called when the `ImageDownloader` completes a downloading request with success or failure.
///
/// - Parameters:
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
/// - url: URL of the original request URL.
/// - response: The response object of the downloading process.
/// - error: The error in case of failure.
///
func imageDownloader(
_ downloader: ImageDownloader,
didFinishDownloadingImageForURL url: URL,
with response: URLResponse?,
error: Error?)
/// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is
/// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition
/// processing on the image data.
///
/// - Parameters:
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
/// - data: The original downloaded data.
/// - dataTask: The data task contains request and response information of the download.
/// - Note:
/// This can be used to pre-process raw image data before creation of `Image` instance (i.e.
/// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with
/// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image
/// processing flow if you find the data is corrupted or malformed.
///
/// If this method is implemented, `imageDownloader(_:didDownload:for:)` will not be called anymore.
func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with dataTask: SessionDataTask) -> Data?
/// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is
/// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition
/// processing on the image data.
///
/// - Parameters:
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
/// - data: The original downloaded data.
/// - url: The URL of the original request URL.
/// - Returns: The data from which Kingfisher should use to create an image. You need to provide valid data
/// which content is one of the supported image file format. Kingfisher will perform process on this
/// data and try to convert it to an image object.
/// - Note:
/// This can be used to pre-process raw image data before creation of `Image` instance (i.e.
/// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with
/// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image
/// processing flow if you find the data is corrupted or malformed.
///
/// If `imageDownloader(_:didDownload:with:)` is implemented, this method will not be called anymore.
func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data?
/// Called when the `ImageDownloader` object successfully downloads and processes an image from specified URL.
///
/// - Parameters:
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
/// - image: The downloaded and processed image.
/// - url: URL of the original request URL.
/// - response: The original response object of the downloading process.
///
func imageDownloader(
_ downloader: ImageDownloader,
didDownload image: KFCrossPlatformImage,
for url: URL,
with response: URLResponse?)
/// Checks if a received HTTP status code is valid or not.
/// By default, a status code in range 200..<400 is considered as valid.
/// If an invalid code is received, the downloader will raise an `KingfisherError` with
/// `ResponseErrorReason.invalidHTTPStatusCode` as its reason.
///
/// - Parameters:
/// - code: The received HTTP status code.
/// - downloader: The `ImageDownloader` object asks for validate status code.
/// - Returns: Returns a value to indicate whether this HTTP status code is valid or not.
/// - Note: If the default 200 to 400 valid code does not suit your need,
/// you can implement this method to change that behavior.
func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool
/// Called when the task has received a valid HTTP response after it passes other checks such as the status code.
/// You can perform additional checks or verification on the response to determine if the download should be allowed.
///
/// For example, it is useful if you want to verify some header values in the response before actually starting the
/// download.
///
/// If implemented, it is your responsibility to call the `completionHandler` with a proper response disposition,
/// such as `.allow` to start the actual downloading or `.cancel` to cancel the task. If `.cancel` is used as the
/// disposition, the downloader will raise an `KingfisherError` with
/// `ResponseErrorReason.cancelledByDelegate` as its reason. If not implemented, any response which passes other
/// checked will be allowed and the download starts.
///
/// - Parameters:
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
/// - response: The original response object of the downloading process.
/// - completionHandler: A completion handler that receives the disposition for the download task. You must call
/// this handler with either `.allow` or `.cancel`.
func imageDownloader(
_ downloader: ImageDownloader,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
}
// Default implementation for `ImageDownloaderDelegate`.
extension ImageDownloaderDelegate {
public func imageDownloader(
_ downloader: ImageDownloader,
willDownloadImageForURL url: URL,
with request: URLRequest?) {}
public func imageDownloader(
_ downloader: ImageDownloader,
didFinishDownloadingImageForURL url: URL,
with response: URLResponse?,
error: Error?) {}
public func imageDownloader(
_ downloader: ImageDownloader,
didDownload image: KFCrossPlatformImage,
for url: URL,
with response: URLResponse?) {}
public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {
return (200..<400).contains(code)
}
public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? {
guard let url = task.originalURL else {
return data
}
return imageDownloader(downloader, didDownload: data, for: url)
}
public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {
return data
}
public func imageDownloader(
_ downloader: ImageDownloader,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
completionHandler(.allow)
}
}

View File

@@ -0,0 +1,116 @@
//
// ImageModifier.swift
// Kingfisher
//
// Created by Ethan Gill on 2017/11/28.
//
// Copyright (c) 2019 Ethan Gill <ethan.gill@me.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// An `ImageModifier` can be used to change properties on an image between cache serialization and the actual use of
/// the image. The `modify(_:)` method will be called after the image retrieved from its source and before it returned
/// to the caller. This modified image is expected to be only used for rendering purpose, any changes applied by the
/// `ImageModifier` will not be serialized or cached.
public protocol ImageModifier {
/// Modify an input `Image`.
///
/// - parameter image: Image which will be modified by `self`
///
/// - returns: The modified image.
///
/// - Note: The return value will be unmodified if modifying is not possible on
/// the current platform.
/// - Note: Most modifiers support UIImage or NSImage, but not CGImage.
func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage
}
/// A wrapper for creating an `ImageModifier` easier.
/// This type conforms to `ImageModifier` and wraps an image modify block.
/// If the `block` throws an error, the original image will be used.
public struct AnyImageModifier: ImageModifier {
/// A block which modifies images, or returns the original image
/// if modification cannot be performed with an error.
let block: (KFCrossPlatformImage) throws -> KFCrossPlatformImage
/// Creates an `AnyImageModifier` with a given `modify` block.
public init(modify: @escaping (KFCrossPlatformImage) throws -> KFCrossPlatformImage) {
block = modify
}
/// Modify an input `Image`. See `ImageModifier` protocol for more.
public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
return (try? block(image)) ?? image
}
}
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
/// Modifier for setting the rendering mode of images.
public struct RenderingModeImageModifier: ImageModifier {
/// The rendering mode to apply to the image.
public let renderingMode: UIImage.RenderingMode
/// Creates a `RenderingModeImageModifier`.
///
/// - Parameter renderingMode: The rendering mode to apply to the image. Default is `.automatic`.
public init(renderingMode: UIImage.RenderingMode = .automatic) {
self.renderingMode = renderingMode
}
/// Modify an input `Image`. See `ImageModifier` protocol for more.
public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
return image.withRenderingMode(renderingMode)
}
}
/// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images.
public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier {
/// Creates a `FlipsForRightToLeftLayoutDirectionImageModifier`.
public init() {}
/// Modify an input `Image`. See `ImageModifier` protocol for more.
public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
return image.imageFlippedForRightToLeftLayoutDirection()
}
}
/// Modifier for setting the `alignmentRectInsets` property of images.
public struct AlignmentRectInsetsImageModifier: ImageModifier {
/// The alignment insets to apply to the image
public let alignmentInsets: UIEdgeInsets
/// Creates an `AlignmentRectInsetsImageModifier`.
public init(alignmentInsets: UIEdgeInsets) {
self.alignmentInsets = alignmentInsets
}
/// Modify an input `Image`. See `ImageModifier` protocol for more.
public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
return image.withAlignmentRectInsets(alignmentInsets)
}
}
#endif

View File

@@ -0,0 +1,442 @@
//
// ImagePrefetcher.swift
// Kingfisher
//
// Created by Claire Knight <claire.knight@moggytech.co.uk> on 24/02/2016
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if os(macOS)
import AppKit
#else
import UIKit
#endif
/// Progress update block of prefetcher when initialized with a list of resources.
///
/// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while
/// downloading, encountered an error when downloading or the download not being started at all.
/// - `completedResources`: An array of resources that are downloaded and cached successfully.
public typealias PrefetcherProgressBlock =
((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void)
/// Progress update block of prefetcher when initialized with a list of resources.
///
/// - `skippedSources`: An array of sources that are already cached before the prefetching starting.
/// - `failedSources`: An array of sources that fail to be fetched.
/// - `completedResources`: An array of sources that are fetched and cached successfully.
public typealias PrefetcherSourceProgressBlock =
((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)
/// Completion block of prefetcher when initialized with a list of sources.
///
/// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while
/// downloading, encountered an error when downloading or the download not being started at all.
/// - `completedResources`: An array of resources that are downloaded and cached successfully.
public typealias PrefetcherCompletionHandler =
((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void)
/// Completion block of prefetcher when initialized with a list of sources.
///
/// - `skippedSources`: An array of sources that are already cached before the prefetching starting.
/// - `failedSources`: An array of sources that fail to be fetched.
/// - `completedSources`: An array of sources that are fetched and cached successfully.
public typealias PrefetcherSourceCompletionHandler =
((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)
/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them.
/// This is useful when you know a list of image resources and want to download them before showing. It also works with
/// some Cocoa prefetching mechanism like table view or collection view `prefetchDataSource`, to start image downloading
/// and caching before they display on screen.
public class ImagePrefetcher: CustomStringConvertible {
public var description: String {
return "\(Unmanaged.passUnretained(self).toOpaque())"
}
/// The maximum concurrent downloads to use when prefetching images. Default is 5.
public var maxConcurrentDownloads = 5
private let prefetchSources: [Source]
private let optionsInfo: KingfisherParsedOptionsInfo
private var progressBlock: PrefetcherProgressBlock?
private var completionHandler: PrefetcherCompletionHandler?
private var progressSourceBlock: PrefetcherSourceProgressBlock?
private var completionSourceHandler: PrefetcherSourceCompletionHandler?
private var tasks = [String: DownloadTask.WrappedTask]()
private var pendingSources: ArraySlice<Source>
private var skippedSources = [Source]()
private var completedSources = [Source]()
private var failedSources = [Source]()
private var stopped = false
// A manager used for prefetching. We will use the helper methods in manager.
private let manager: KingfisherManager
private let pretchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.pretchQueue")
private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue")
private var finished: Bool {
let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count
return totalFinished == prefetchSources.count && tasks.isEmpty
}
/// Creates an image prefetcher with an array of URLs.
///
/// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.
/// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process.
/// The images which are already cached will be skipped without downloading again.
///
/// - Parameters:
/// - urls: The URLs which should be prefetched.
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
/// - progressBlock: Called every time an resource is downloaded, skipped or cancelled.
/// - completionHandler: Called when the whole prefetching process finished.
///
/// - Note:
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
/// the downloader and cache target respectively. You can specify another downloader or cache by using
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
public convenience init(
urls: [URL],
options: KingfisherOptionsInfo? = nil,
progressBlock: PrefetcherProgressBlock? = nil,
completionHandler: PrefetcherCompletionHandler? = nil)
{
let resources: [Resource] = urls.map { $0 }
self.init(
resources: resources,
options: options,
progressBlock: progressBlock,
completionHandler: completionHandler)
}
/// Creates an image prefetcher with an array of URLs.
///
/// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.
/// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process.
/// The images which are already cached will be skipped without downloading again.
///
/// - Parameters:
/// - urls: The URLs which should be prefetched.
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
/// - completionHandler: Called when the whole prefetching process finished.
///
/// - Note:
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
/// the downloader and cache target respectively. You can specify another downloader or cache by using
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
public convenience init(
urls: [URL],
options: KingfisherOptionsInfo? = nil,
completionHandler: PrefetcherCompletionHandler? = nil)
{
let resources: [Resource] = urls.map { $0 }
self.init(
resources: resources,
options: options,
progressBlock: nil,
completionHandler: completionHandler)
}
/// Creates an image prefetcher with an array of resources.
///
/// - Parameters:
/// - resources: The resources which should be prefetched. See `Resource` type for more.
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
/// - progressBlock: Called every time an resource is downloaded, skipped or cancelled.
/// - completionHandler: Called when the whole prefetching process finished.
///
/// - Note:
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
/// the downloader and cache target respectively. You can specify another downloader or cache by using
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
public convenience init(
resources: [Resource],
options: KingfisherOptionsInfo? = nil,
progressBlock: PrefetcherProgressBlock? = nil,
completionHandler: PrefetcherCompletionHandler? = nil)
{
self.init(sources: resources.map { $0.convertToSource() }, options: options)
self.progressBlock = progressBlock
self.completionHandler = completionHandler
}
/// Creates an image prefetcher with an array of resources.
///
/// - Parameters:
/// - resources: The resources which should be prefetched. See `Resource` type for more.
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
/// - completionHandler: Called when the whole prefetching process finished.
///
/// - Note:
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
/// the downloader and cache target respectively. You can specify another downloader or cache by using
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
public convenience init(
resources: [Resource],
options: KingfisherOptionsInfo? = nil,
completionHandler: PrefetcherCompletionHandler? = nil)
{
self.init(sources: resources.map { $0.convertToSource() }, options: options)
self.completionHandler = completionHandler
}
/// Creates an image prefetcher with an array of sources.
///
/// - Parameters:
/// - sources: The sources which should be prefetched. See `Source` type for more.
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
/// - progressBlock: Called every time an source fetching successes, fails, is skipped.
/// - completionHandler: Called when the whole prefetching process finished.
///
/// - Note:
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
/// the downloader and cache target respectively. You can specify another downloader or cache by using
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
public convenience init(sources: [Source],
options: KingfisherOptionsInfo? = nil,
progressBlock: PrefetcherSourceProgressBlock? = nil,
completionHandler: PrefetcherSourceCompletionHandler? = nil)
{
self.init(sources: sources, options: options)
self.progressSourceBlock = progressBlock
self.completionSourceHandler = completionHandler
}
/// Creates an image prefetcher with an array of sources.
///
/// - Parameters:
/// - sources: The sources which should be prefetched. See `Source` type for more.
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
/// - completionHandler: Called when the whole prefetching process finished.
///
/// - Note:
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
/// the downloader and cache target respectively. You can specify another downloader or cache by using
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
public convenience init(sources: [Source],
options: KingfisherOptionsInfo? = nil,
completionHandler: PrefetcherSourceCompletionHandler? = nil)
{
self.init(sources: sources, options: options)
self.completionSourceHandler = completionHandler
}
init(sources: [Source], options: KingfisherOptionsInfo?) {
var options = KingfisherParsedOptionsInfo(options)
prefetchSources = sources
pendingSources = ArraySlice(sources)
// We want all callbacks from our prefetch queue, so we should ignore the callback queue in options.
// Add our own callback dispatch queue to make sure all internal callbacks are
// coming back in our expected queue.
options.callbackQueue = .dispatch(pretchQueue)
optionsInfo = options
let cache = optionsInfo.targetCache ?? .default
let downloader = optionsInfo.downloader ?? .default
manager = KingfisherManager(downloader: downloader, cache: cache)
}
/// Starts to download the resources and cache them. This can be useful for background downloading
/// of assets that are required for later use in an app. This code will not try and update any UI
/// with the results of the process.
public func start() {
pretchQueue.async {
guard !self.stopped else {
assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.")
self.handleComplete()
return
}
guard self.maxConcurrentDownloads > 0 else {
assertionFailure("There should be concurrent downloads value should be at least 1.")
self.handleComplete()
return
}
// Empty case.
guard self.prefetchSources.count > 0 else {
self.handleComplete()
return
}
let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads)
for _ in 0 ..< initialConcurrentDownloads {
if let resource = self.pendingSources.popFirst() {
self.startPrefetching(resource)
}
}
}
}
/// Stops current downloading progress, and cancel any future prefetching activity that might be occuring.
public func stop() {
pretchQueue.async {
if self.finished { return }
self.stopped = true
self.tasks.values.forEach { $0.cancel() }
}
}
private func downloadAndCache(_ source: Source) {
let downloadTaskCompletionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void) = { result in
self.tasks.removeValue(forKey: source.cacheKey)
do {
let _ = try result.get()
self.completedSources.append(source)
} catch {
self.failedSources.append(source)
}
self.reportProgress()
if self.stopped {
if self.tasks.isEmpty {
self.failedSources.append(contentsOf: self.pendingSources)
self.handleComplete()
}
} else {
self.reportCompletionOrStartNext()
}
}
var downloadTask: DownloadTask.WrappedTask?
ImagePrefetcher.requestingQueue.sync {
let context = RetrievingContext(
options: optionsInfo, originalSource: source
)
downloadTask = manager.loadAndCacheImage(
source: source,
context: context,
completionHandler: downloadTaskCompletionHandler)
}
if let downloadTask = downloadTask {
tasks[source.cacheKey] = downloadTask
}
}
private func append(cached source: Source) {
skippedSources.append(source)
reportProgress()
reportCompletionOrStartNext()
}
private func startPrefetching(_ source: Source)
{
if optionsInfo.forceRefresh {
downloadAndCache(source)
return
}
let cacheType = manager.cache.imageCachedType(
forKey: source.cacheKey,
processorIdentifier: optionsInfo.processor.identifier)
switch cacheType {
case .memory:
append(cached: source)
case .disk:
if optionsInfo.alsoPrefetchToMemory {
let context = RetrievingContext(options: optionsInfo, originalSource: source)
_ = manager.retrieveImageFromCache(
source: source,
context: context)
{
_ in
self.append(cached: source)
}
} else {
append(cached: source)
}
case .none:
downloadAndCache(source)
}
}
private func reportProgress() {
if progressBlock == nil && progressSourceBlock == nil {
return
}
let skipped = self.skippedSources
let failed = self.failedSources
let completed = self.completedSources
CallbackQueue.mainCurrentOrAsync.execute {
self.progressSourceBlock?(skipped, failed, completed)
self.progressBlock?(
skipped.compactMap { $0.asResource },
failed.compactMap { $0.asResource },
completed.compactMap { $0.asResource }
)
}
}
private func reportCompletionOrStartNext() {
if let resource = self.pendingSources.popFirst() {
// Loose call stack for huge ammount of sources.
pretchQueue.async { self.startPrefetching(resource) }
} else {
guard allFinished else { return }
self.handleComplete()
}
}
var allFinished: Bool {
return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count
}
private func handleComplete() {
if completionHandler == nil && completionSourceHandler == nil {
return
}
// The completion handler should be called on the main thread
CallbackQueue.mainCurrentOrAsync.execute {
self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources)
self.completionHandler?(
self.skippedSources.compactMap { $0.asResource },
self.failedSources.compactMap { $0.asResource },
self.completedSources.compactMap { $0.asResource }
)
self.completionHandler = nil
self.progressBlock = nil
}
}
}

View File

@@ -0,0 +1,76 @@
//
// RedirectHandler.swift
// Kingfisher
//
// Created by Roman Maidanovych on 2018/12/10.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// Represents and wraps a method for modifying request during an image download request redirection.
public protocol ImageDownloadRedirectHandler {
/// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection.
/// This is the posibility you can modify the image download request during redirection. 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.
///
/// Usually, you pass an `ImageDownloadRedirectHandler` as the associated value of
/// `KingfisherOptionsInfoItem.redirectHandler` and use it as the `options` parameter in related methods.
///
/// If you do nothing with the input `request` and return it as is, a downloading process will redirect with it.
///
/// - Parameters:
/// - task: The current `SessionDataTask` which triggers this redirect.
/// - response: The response received during redirection.
/// - newRequest: The request for redirection which can be modified.
/// - completionHandler: A closure for being called with modified request.
func handleHTTPRedirection(
for task: SessionDataTask,
response: HTTPURLResponse,
newRequest: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void)
}
/// A wrapper for creating an `ImageDownloadRedirectHandler` easier.
/// This type conforms to `ImageDownloadRedirectHandler` and wraps a redirect request modify block.
public struct AnyRedirectHandler: ImageDownloadRedirectHandler {
let block: (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void
public func handleHTTPRedirection(
for task: SessionDataTask,
response: HTTPURLResponse,
newRequest: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void)
{
block(task, response, newRequest, completionHandler)
}
/// Creates a value of `ImageDownloadRedirectHandler` which runs `modify` block.
///
/// - Parameter modify: The request modifying block runs when a request modifying task comes.
///
public init(handle: @escaping (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void) {
block = handle
}
}

View File

@@ -0,0 +1,108 @@
//
// RequestModifier.swift
// Kingfisher
//
// Created by Wei Wang on 2016/09/05.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// Represents and wraps a method for modifying request before an image download request starts in an asynchronous way.
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.
///
/// Usually, you pass an `AsyncImageDownloadRequestModifier` as the associated value of
/// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods.
///
/// If you do nothing with the input `request` and return it as is, a downloading process will start with it.
///
/// - 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)
/// A block will be called when the download task started.
///
/// 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.
var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { get }
}
/// Represents and wraps a method for modifying request before an image download request starts.
public protocol ImageDownloadRequestModifier: 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.
///
/// Usually, you pass an `ImageDownloadRequestModifier` as the associated value of
/// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods.
///
/// If you do nothing with the input `request` and return it as is, a downloading process will start with it.
///
/// - Parameter request: The input request contains necessary information like `url`. This request is generated
/// according to your resource url as a GET request.
/// - Returns: A modified version of request, which you wish to use for downloading an image. If `nil` returned,
/// a `KingfisherError.requestError` with `.emptyRequest` as its reason will occur.
///
func modified(for request: URLRequest) -> URLRequest?
}
extension ImageDownloadRequestModifier {
public func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) {
let request = modified(for: request)
reportModified(request)
}
/// This is `nil` for a sync `ImageDownloadRequestModifier` by default. You can get the `DownloadTask` from the
/// return value of downloader method.
public var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { return nil }
}
/// A wrapper for creating an `ImageDownloadRequestModifier` easier.
/// This type conforms to `ImageDownloadRequestModifier` and wraps an image modify block.
public struct AnyModifier: ImageDownloadRequestModifier {
let block: (URLRequest) -> URLRequest?
/// For `ImageDownloadRequestModifier` conformation.
public func modified(for request: URLRequest) -> URLRequest? {
return block(request)
}
/// Creates a value of `ImageDownloadRequestModifier` which runs `modify` block.
///
/// - Parameter modify: The request modifying block runs when a request modifying task comes.
/// The return `URLRequest?` value of this block will be used as the image download request.
/// If `nil` returned, a `KingfisherError.requestError` with `.emptyRequest` as its
/// reason will occur.
public init(modify: @escaping (URLRequest) -> URLRequest?) {
block = modify
}
}

View File

@@ -0,0 +1,153 @@
//
// RetryStrategy.swift
// Kingfisher
//
// Created by onevcat on 2020/05/04.
//
// Copyright (c) 2020 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// Represents a retry context which could be used to determine the current retry status.
public class RetryContext {
/// The source from which the target image should be retrieved.
public let source: Source
/// The last error which caused current retry behavior.
public let error: KingfisherError
/// The retried count before current retry happens. This value is `0` if the current retry is for the first time.
public var retriedCount: Int
/// A user set value for passing any other information during the retry. If you choose to use `RetryDecision.retry`
/// as the retry decision for `RetryStrategy.retry(context:retryHandler:)`, the associated value of
/// `RetryDecision.retry` will be delivered to you in the next retry.
public internal(set) var userInfo: Any? = nil
init(source: Source, error: KingfisherError) {
self.source = source
self.error = error
self.retriedCount = 0
}
@discardableResult
func increaseRetryCount() -> RetryContext {
retriedCount += 1
return self
}
}
/// Represents decision of behavior on the current retry.
public enum RetryDecision {
/// A retry should happen. The associated `userInfo` will be pass to the next retry in the `RetryContext` parameter.
case retry(userInfo: Any?)
/// There should be no more retry attempt. The image retrieving process will fail with an error.
case stop
}
/// Defines a retry strategy can be applied to a `.retryStrategy` option.
public protocol RetryStrategy {
/// Kingfisher calls this method if an error happens during the image retrieving process from a `KingfisherManager`.
/// You implement this method to provide necessary logic based on the `context` parameter. Then you need to call
/// `retryHandler` to pass the retry decision back to Kingfisher.
///
/// - Parameters:
/// - context: The retry context containing information of current retry attempt.
/// - retryHandler: A block you need to call with a decision of whether the retry should happen or not.
func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void)
}
/// A retry strategy that guides Kingfisher to retry when a `.responseError` happens, with a specified max retry count
/// and a certain interval mechanism.
public struct DelayRetryStrategy: RetryStrategy {
/// Represents the interval mechanism which used in a `DelayRetryStrategy`.
public enum Interval {
/// The next retry attempt should happen in fixed seconds. For example, if the associated value is 3, the
/// attempts happens after 3 seconds after the previous decision is made.
case seconds(TimeInterval)
/// The next retry attempt should happen in an accumulated duration. For example, if the associated value is 3,
/// the attempts happens with interval of 3, 6, 9, 12, ... seconds.
case accumulated(TimeInterval)
/// Uses a block to determine the next interval. The current retry count is given as a parameter.
case custom(block: (_ retriedCount: Int) -> TimeInterval)
func timeInterval(for retriedCount: Int) -> TimeInterval {
let retryAfter: TimeInterval
switch self {
case .seconds(let interval):
retryAfter = interval
case .accumulated(let interval):
retryAfter = Double(retriedCount + 1) * interval
case .custom(let block):
retryAfter = block(retriedCount)
}
return retryAfter
}
}
/// The max retry count defined for the retry strategy
public let maxRetryCount: Int
/// The retry interval mechanism defined for the retry strategy.
public let retryInterval: Interval
/// Creates a delay retry strategy.
/// - Parameters:
/// - maxRetryCount: The max retry count.
/// - retryInterval: The retry interval mechanism. By default, `.seconds(3)` is used to provide a constant retry
/// interval.
public init(maxRetryCount: Int, retryInterval: Interval = .seconds(3)) {
self.maxRetryCount = maxRetryCount
self.retryInterval = retryInterval
}
public func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) {
// Retry count exceeded.
guard context.retriedCount < maxRetryCount else {
retryHandler(.stop)
return
}
// User cancel the task. No retry.
guard !context.error.isTaskCancelled else {
retryHandler(.stop)
return
}
// Only retry for a response error.
guard case KingfisherError.responseError = context.error else {
retryHandler(.stop)
return
}
let interval = retryInterval.timeInterval(for: context.retriedCount)
if interval == 0 {
retryHandler(.retry(userInfo: nil))
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
retryHandler(.retry(userInfo: nil))
}
}
}
}

View File

@@ -0,0 +1,127 @@
//
// SessionDataTask.swift
// Kingfisher
//
// Created by Wei Wang on 2018/11/1.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// Represents a session data task in `ImageDownloader`. It consists of an underlying `URLSessionDataTask` and
/// an array of `TaskCallback`. Multiple `TaskCallback`s could be added for a single downloading data task.
public class SessionDataTask {
/// Represents the type of token which used for cancelling a task.
public typealias CancelToken = Int
struct TaskCallback {
let onCompleted: Delegate<Result<ImageLoadingResult, KingfisherError>, Void>?
let options: KingfisherParsedOptionsInfo
}
/// Downloaded raw data of current task.
public private(set) var mutableData: Data
// This is a copy of `task.originalRequest?.url`. It is for getting a race-safe behavior for a pitfall on iOS 13.
// Ref: https://github.com/onevcat/Kingfisher/issues/1511
public let originalURL: URL?
/// The underlying download task. It is only for debugging purpose when you encountered an error. You should not
/// modify the content of this task or start it yourself.
public let task: URLSessionDataTask
private var callbacksStore = [CancelToken: TaskCallback]()
var callbacks: [SessionDataTask.TaskCallback] {
lock.lock()
defer { lock.unlock() }
return Array(callbacksStore.values)
}
private var currentToken = 0
private let lock = NSLock()
let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>()
let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>()
var started = false
var containsCallbacks: Bool {
// We should be able to use `task.state != .running` to check it.
// However, in some rare cases, cancelling the task does not change
// task state to `.cancelling` immediately, but still in `.running`.
// So we need to check callbacks count to for sure that it is safe to remove the
// task in delegate.
return !callbacks.isEmpty
}
init(task: URLSessionDataTask) {
self.task = task
self.originalURL = task.originalRequest?.url
mutableData = Data()
}
func addCallback(_ callback: TaskCallback) -> CancelToken {
lock.lock()
defer { lock.unlock() }
callbacksStore[currentToken] = callback
defer { currentToken += 1 }
return currentToken
}
func removeCallback(_ token: CancelToken) -> TaskCallback? {
lock.lock()
defer { lock.unlock() }
if let callback = callbacksStore[token] {
callbacksStore[token] = nil
return callback
}
return nil
}
func removeAllCallbacks() -> Void {
lock.lock()
defer { lock.unlock() }
callbacksStore.removeAll()
}
func resume() {
guard !started else { return }
started = true
task.resume()
}
func cancel(token: CancelToken) {
guard let callback = removeCallback(token) else {
return
}
onCallbackCancelled.call((token, callback))
}
func forceCancel() {
for token in callbacksStore.keys {
cancel(token: token)
}
}
func didReceiveData(_ data: Data) {
mutableData.append(data)
}
}

View File

@@ -0,0 +1,271 @@
//
// SessionDelegate.swift
// Kingfisher
//
// Created by Wei Wang on 2018/11/1.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
// Represents the delegate object of downloader session. It also behave like a task manager for downloading.
@objc(KFSessionDelegate) // Fix for ObjC header name conflicting. https://github.com/onevcat/Kingfisher/issues/1530
open class SessionDelegate: NSObject {
typealias SessionChallengeFunc = (
URLSession,
URLAuthenticationChallenge,
(URLSession.AuthChallengeDisposition, URLCredential?) -> Void
)
typealias SessionTaskChallengeFunc = (
URLSession,
URLSessionTask,
URLAuthenticationChallenge,
(URLSession.AuthChallengeDisposition, URLCredential?) -> Void
)
private var tasks: [URL: SessionDataTask] = [:]
private let lock = NSLock()
let onValidStatusCode = Delegate<Int, Bool>()
let onResponseReceived = Delegate<(URLResponse, (URLSession.ResponseDisposition) -> Void), Void>()
let onDownloadingFinished = Delegate<(URL, Result<URLResponse, KingfisherError>), Void>()
let onDidDownloadData = Delegate<SessionDataTask, Data?>()
let onReceiveSessionChallenge = Delegate<SessionChallengeFunc, Void>()
let onReceiveSessionTaskChallenge = Delegate<SessionTaskChallengeFunc, Void>()
func add(
_ dataTask: URLSessionDataTask,
url: URL,
callback: SessionDataTask.TaskCallback) -> DownloadTask
{
lock.lock()
defer { lock.unlock() }
// Create a new task if necessary.
let task = SessionDataTask(task: dataTask)
task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in
guard let task = task else { return }
let (token, callback) = value
let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token))
task.onTaskDone.call((.failure(error), [callback]))
// No other callbacks waiting, we can clear the task now.
if !task.containsCallbacks {
let dataTask = task.task
self.cancelTask(dataTask)
self.remove(task)
}
}
let token = task.addCallback(callback)
tasks[url] = task
return DownloadTask(sessionTask: task, cancelToken: token)
}
private func cancelTask(_ dataTask: URLSessionDataTask) {
lock.lock()
defer { lock.unlock() }
dataTask.cancel()
}
func append(
_ task: SessionDataTask,
callback: SessionDataTask.TaskCallback) -> DownloadTask
{
let token = task.addCallback(callback)
return DownloadTask(sessionTask: task, cancelToken: token)
}
private func remove(_ task: SessionDataTask) {
lock.lock()
defer { lock.unlock() }
guard let url = task.originalURL else {
return
}
task.removeAllCallbacks()
tasks[url] = nil
}
private func task(for task: URLSessionTask) -> SessionDataTask? {
lock.lock()
defer { lock.unlock() }
guard let url = task.originalRequest?.url else {
return nil
}
guard let sessionTask = tasks[url] else {
return nil
}
guard sessionTask.task.taskIdentifier == task.taskIdentifier else {
return nil
}
return sessionTask
}
func task(for url: URL) -> SessionDataTask? {
lock.lock()
defer { lock.unlock() }
return tasks[url]
}
func cancelAll() {
lock.lock()
let taskValues = tasks.values
lock.unlock()
for task in taskValues {
task.forceCancel()
}
}
func cancel(url: URL) {
lock.lock()
let task = tasks[url]
lock.unlock()
task?.forceCancel()
}
}
extension SessionDelegate: URLSessionDataDelegate {
open func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
{
guard let httpResponse = response as? HTTPURLResponse else {
let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response))
onCompleted(task: dataTask, result: .failure(error))
completionHandler(.cancel)
return
}
let httpStatusCode = httpResponse.statusCode
guard onValidStatusCode.call(httpStatusCode) == true else {
let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse))
onCompleted(task: dataTask, result: .failure(error))
completionHandler(.cancel)
return
}
let inspectedHandler: (URLSession.ResponseDisposition) -> Void = { disposition in
if disposition == .cancel {
let error = KingfisherError.responseError(reason: .cancelledByDelegate(response: response))
self.onCompleted(task: dataTask, result: .failure(error))
}
completionHandler(disposition)
}
onResponseReceived.call((response, inspectedHandler))
}
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let task = self.task(for: dataTask) else {
return
}
task.didReceiveData(data)
task.callbacks.forEach { callback in
callback.options.onDataReceived?.forEach { sideEffect in
sideEffect.onDataReceived(session, task: task, data: data)
}
}
}
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let sessionTask = self.task(for: task) else { return }
if let url = sessionTask.originalURL {
let result: Result<URLResponse, KingfisherError>
if let error = error {
result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
} else if let response = task.response {
result = .success(response)
} else {
result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask)))
}
onDownloadingFinished.call((url, result))
}
let result: Result<(Data, URLResponse?), KingfisherError>
if let error = error {
result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
} else {
if let data = onDidDownloadData.call(sessionTask) {
result = .success((data, task.response))
} else {
result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask)))
}
}
onCompleted(task: task, result: result)
}
open func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
onReceiveSessionChallenge.call((session, challenge, completionHandler))
}
open func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler))
}
open func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void)
{
guard let sessionDataTask = self.task(for: task),
let redirectHandler = Array(sessionDataTask.callbacks).last?.options.redirectHandler else
{
completionHandler(request)
return
}
redirectHandler.handleHTTPRedirection(
for: sessionDataTask,
response: response,
newRequest: request,
completionHandler: completionHandler)
}
private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?), KingfisherError>) {
guard let sessionTask = self.task(for: task) else {
return
}
sessionTask.onTaskDone.call((result, sessionTask.callbacks))
remove(sessionTask)
}
}