| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- //
- // ImageDownloader.swift
- // Kingfisher
- //
- // Created by Wei Wang on 15/4/6.
- //
- // Copyright (c) 2018 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
- /// Represents a success result of an image downloading progess.
- public struct ImageDownloadResult {
- /// The downloaded image.
- public let image: Image
- /// Original URL of the image request.
- public let url: URL
- /// The raw data received from downloader.
- public let originalData: Data
- }
- /// 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)
- }
- }
- /// Represents a downloading manager for requesting the image with a URL from server.
- open class ImageDownloader {
- /// The default downloader.
- public static let `default` = ImageDownloader(name: "default")
- // MARK: - Public property
- /// 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)
- }
- }
-
- /// Whether the download requests should use pipline 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: AuthenticationChallengeResponsable?
- private let name: String
- private let sessionDelegate: SessionDelegate
- private var session: URLSession
- /// 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.onDownloadingFinished.delegate(on: self) { (self, value) in
- let (url, result) = value
- self.delegate?.imageDownloader(
- self, didFinishDownloadingImageForURL: url, with: result.value, error: result.error)
- }
- sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
- guard let url = task.task.originalRequest?.url else {
- return task.mutableData
- }
- return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, for: url)
- }
- }
- /// 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<ImageDownloadResult, KingfisherError>) -> Void)? = nil)
- -> DownloadTask?
- {
- // Creates default request.
- var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
- request.httpShouldUsePipelining = requestsUsePipelining
- let options = options ?? .empty
- // Modifies request before sending.
- guard let r = options.modifier.modified(for: request) else {
- options.callbackQueue.execute {
- completionHandler?(.failure(KingfisherError.requestError(reason: .emptyRequest)))
- }
- return nil
- }
- request = r
-
- // There is a possibility that request modifier changed the url to `nil` or empty.
- // In this case, throw an error.
- guard let url = request.url, !url.absoluteString.isEmpty else {
- options.callbackQueue.execute {
- completionHandler?(.failure(KingfisherError.requestError(reason: .invalidURL(request: request))))
- }
- return nil
- }
- // Wraps `progressBlock` and `completionHandler` to `onProgress` and `onCompleted` respectively.
- let onProgress = progressBlock.map {
- block -> Delegate<(Int64, Int64), Void> in
- let delegate = Delegate<(Int64, Int64), Void>()
- delegate.delegate(on: self) { (_, progress) in
- let (downloaded, total) = progress
- block(downloaded, total)
- }
- return delegate
- }
- let onCompleted = completionHandler.map {
- block -> Delegate<Result<ImageDownloadResult, KingfisherError>, Void> in
- let delegate = Delegate<Result<ImageDownloadResult, KingfisherError>, Void>()
- delegate.delegate(on: self) { (_, result) in
- block(result)
- }
- return delegate
- }
- // SessionDataTask.TaskCallback is a wrapper for `onProgress`, `onCompleted` and `options` (for processor info)
- let callback = SessionDataTask.TaskCallback(
- onProgress: onProgress, onCompleted: onCompleted, options: options)
- // Ready to start download. Add it to session task manager (`sessionHandler`)
- let dataTask = session.dataTask(with: request)
- dataTask.priority = options.downloadPriority
- let downloadTask = sessionDelegate.add(dataTask, url: url, callback: callback)
- let sessionTask = downloadTask.sessionTask
- 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.delegate?.imageDownloader(
- self,
- didFinishDownloadingImageForURL: url,
- with: result.value?.1,
- error: result.error)
- switch result {
- // Download finished. Now process the data to an image.
- case .success(let (data, response)):
- let prosessor = ImageDataProcessor(name: self.name, data: data, callbacks: callbacks)
- prosessor.onImageProcessed.delegate(on: self) { (self, result) 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) = result
- if let image = result.value {
- self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
- }
- let imageResult = result.map { ImageDownloadResult(image: $0, url: url, originalData: data) }
- let queue = callback.options.callbackQueue
- queue.execute { callback.onCompleted?.call(imageResult) }
- }
- prosessor.process()
- case .failure(let error):
- callbacks.forEach { callback in
- let queue = callback.options.callbackQueue
- queue.execute { callback.onCompleted?.call(.failure(error)) }
- }
- }
- }
- // Start the session task if not started yet.
- if !sessionTask.started {
- delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
- sessionTask.resume()
- }
- return downloadTask
- }
- }
- // MARK: - Download method
- 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 `AuthenticationChallengeResponsable`.
- extension ImageDownloader: AuthenticationChallengeResponsable {}
- // Use the default implementation from extension of `ImageDownloaderDelegate`.
- extension ImageDownloader: ImageDownloaderDelegate {}
|