AutorÃa | Ultima modificación | Ver Log |
//// ImageDownloader.swift//// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)//// 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 Alamofireimport Foundation#if os(iOS) || os(tvOS) || os(watchOS)import UIKit#elseif os(macOS)import Cocoa#endif/// Alias for `DataResponse<T, AFIError>`.public typealias AFIDataResponse<T> = DataResponse<T, AFIError>/// Alias for `Result<T, AFIError>`.public typealias AFIResult<T> = Result<T, AFIError>/// The `RequestReceipt` is an object vended by the `ImageDownloader` when starting a download request. It can be used/// to cancel active requests running on the `ImageDownloader` session. As a general rule, image download requests/// should be cancelled using the `RequestReceipt` instead of calling `cancel` directly on the `request` itself. The/// `ImageDownloader` is optimized to handle duplicate request scenarios as well as pending versus active downloads.open class RequestReceipt {/// The download request created by the `ImageDownloader`.public let request: DataRequest/// The unique identifier for the image filters and completion handlers when duplicate requests are made.public let receiptID: Stringinit(request: DataRequest, receiptID: String) {self.request = requestself.receiptID = receiptID}}// MARK: -/// The `ImageDownloader` class is responsible for downloading images in parallel on a prioritized queue. Incoming/// downloads are added to the front or back of the queue depending on the download prioritization. Each downloaded/// image is cached in the underlying `NSURLCache` as well as the in-memory image cache that supports image filters./// By default, any download request with a cached image equivalent in the image cache will automatically be served the/// cached image representation. Additional advanced features include supporting multiple image filters and completion/// handlers for a single request.open class ImageDownloader {/// The completion handler closure used when an image download completes.public typealias CompletionHandler = (AFIDataResponse<Image>) -> Void/// The progress handler closure called periodically during an image download.public typealias ProgressHandler = DataRequest.ProgressHandler// MARK: Helper Types/// Defines the order prioritization of incoming download requests being inserted into the queue.////// - fifo: All incoming downloads are added to the back of the queue./// - lifo: All incoming downloads are added to the front of the queue.public enum DownloadPrioritization {case fifo, lifo}final class ResponseHandler {let urlID: Stringlet handlerID: Stringlet request: DataRequestvar operations: [(receiptID: String, filter: ImageFilter?, completion: CompletionHandler?)]init(request: DataRequest,handlerID: String,receiptID: String,filter: ImageFilter?,completion: CompletionHandler?) {self.request = requesturlID = ImageDownloader.urlIdentifier(for: request.convertible)self.handlerID = handlerIDoperations = [(receiptID: receiptID, filter: filter, completion: completion)]}}// MARK: Properties/// The image cache used to store all downloaded images in.public let imageCache: ImageRequestCache?/// The credential used for authenticating each download request.open private(set) var credential: URLCredential?/// Response serializer used to convert the image data to UIImage.public var imageResponseSerializer = ImageResponseSerializer()/// The underlying Alamofire `Session` instance used to handle all download requests.public let session: Sessionlet downloadPrioritization: DownloadPrioritizationlet maximumActiveDownloads: Intvar activeRequestCount = 0var queuedRequests: [Request] = []var responseHandlers: [String: ResponseHandler] = [:]private let synchronizationQueue: DispatchQueue = {let name = String(format: "org.alamofire.imagedownloader.synchronizationqueue-%08x%08x", arc4random(), arc4random())return DispatchQueue(label: name)}()private let responseQueue: DispatchQueue = {let name = String(format: "org.alamofire.imagedownloader.responsequeue-%08x%08x", arc4random(), arc4random())return DispatchQueue(label: name, attributes: .concurrent)}()// MARK: Initialization/// The default instance of `ImageDownloader` initialized with default values.public static let `default` = ImageDownloader()/// Creates a default `URLSessionConfiguration` with common usage parameter values.////// - returns: The default `URLSessionConfiguration` instance.open class func defaultURLSessionConfiguration() -> URLSessionConfiguration {let configuration = URLSessionConfiguration.defaultconfiguration.headers = .defaultconfiguration.httpShouldSetCookies = trueconfiguration.httpShouldUsePipelining = falseconfiguration.requestCachePolicy = .useProtocolCachePolicyconfiguration.allowsCellularAccess = trueconfiguration.timeoutIntervalForRequest = 60configuration.urlCache = ImageDownloader.defaultURLCache()return configuration}/// Creates a default `URLCache` with common usage parameter values.////// - returns: The default `URLCache` instance.open class func defaultURLCache() -> URLCache {let memoryCapacity = 20 * 1024 * 1024let diskCapacity = 150 * 1024 * 1024let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).firstlet imageDownloaderPath = "org.alamofire.imagedownloader"#if targetEnvironment(macCatalyst)return URLCache(memoryCapacity: memoryCapacity,diskCapacity: diskCapacity,directory: cacheDirectory?.appendingPathComponent(imageDownloaderPath))#else#if os(macOS)return URLCache(memoryCapacity: memoryCapacity,diskCapacity: diskCapacity,diskPath: cacheDirectory?.appendingPathComponent(imageDownloaderPath).path)#elsereturn URLCache(memoryCapacity: memoryCapacity,diskCapacity: diskCapacity,diskPath: imageDownloaderPath)#endif#endif}/// Initializes the `ImageDownloader` instance with the given configuration, download prioritization, maximum active/// download count and image cache.////// - parameter configuration: The `URLSessionConfiguration` to use to create the underlying Alamofire/// `SessionManager` instance./// - parameter downloadPrioritization: The download prioritization of the download queue. `.fifo` by default./// - parameter maximumActiveDownloads: The maximum number of active downloads allowed at any given time./// - parameter imageCache: The image cache used to store all downloaded images in.////// - returns: The new `ImageDownloader` instance.public init(configuration: URLSessionConfiguration = ImageDownloader.defaultURLSessionConfiguration(),downloadPrioritization: DownloadPrioritization = .fifo,maximumActiveDownloads: Int = 4,imageCache: ImageRequestCache? = AutoPurgingImageCache()) {session = Session(configuration: configuration, startRequestsImmediately: false)self.downloadPrioritization = downloadPrioritizationself.maximumActiveDownloads = maximumActiveDownloadsself.imageCache = imageCache}/// Initializes the `ImageDownloader` instance with the given session manager, download prioritization, maximum/// active download count and image cache.////// - parameter session: The Alamofire `Session` instance to handle all download requests./// - parameter downloadPrioritization: The download prioritization of the download queue. `.fifo` by default./// - parameter maximumActiveDownloads: The maximum number of active downloads allowed at any given time./// - parameter imageCache: The image cache used to store all downloaded images in.////// - returns: The new `ImageDownloader` instance.public init(session: Session,downloadPrioritization: DownloadPrioritization = .fifo,maximumActiveDownloads: Int = 4,imageCache: ImageRequestCache? = AutoPurgingImageCache()) {precondition(!session.startRequestsImmediately, "Session must set `startRequestsImmediately` to `false`.")self.session = sessionself.downloadPrioritization = downloadPrioritizationself.maximumActiveDownloads = maximumActiveDownloadsself.imageCache = imageCache}// MARK: Authentication/// Associates an HTTP Basic Auth credential with all future download requests.////// - parameter user: The user./// - parameter password: The password./// - parameter persistence: The URL credential persistence. `.forSession` by default.open func addAuthentication(user: String,password: String,persistence: URLCredential.Persistence = .forSession) {let credential = URLCredential(user: user, password: password, persistence: persistence)addAuthentication(usingCredential: credential)}/// Associates the specified credential with all future download requests.////// - parameter credential: The credential.open func addAuthentication(usingCredential credential: URLCredential) {synchronizationQueue.sync {self.credential = credential}}// MARK: Download/// Creates a download request using the internal Alamofire `SessionManager` instance for the specified URL request.////// If the same download request is already in the queue or currently being downloaded, the filter and completion/// handler are appended to the already existing request. Once the request completes, all filters and completion/// handlers attached to the request are executed in the order they were added. Additionally, any filters attached/// to the request with the same identifiers are only executed once. The resulting image is then passed into each/// completion handler paired with the filter.////// You should not attempt to directly cancel the `request` inside the request receipt since other callers may be/// relying on the completion of that request. Instead, you should call `cancelRequestForRequestReceipt` with the/// returned request receipt to allow the `ImageDownloader` to optimize the cancellation on behalf of all active/// callers.////// - parameter urlRequest: The URL request./// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults to `nil`./// - parameter receiptID: The `identifier` for the `RequestReceipt` returned. Defaults to a new, randomly/// generated UUID./// - parameter serializer: Image response serializer used to convert the image data to `UIImage`. Defaults/// to `nil` which will fall back to the instance `imageResponseSerializer`./// - parameter filter: The image filter to apply to the image after the download is complete. Defaults/// to `nil`./// - parameter progress: The closure to be executed periodically during the lifecycle of the request./// Defaults to `nil`./// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue./// - parameter completion: The closure called when the download request is complete. Defaults to `nil`.////// - returns: The request receipt for the download request if available. `nil` if the image is stored in the image/// cache and the URL request cache policy allows the cache to be used.@discardableResultopen func download(_ urlRequest: URLRequestConvertible,cacheKey: String? = nil,receiptID: String = UUID().uuidString,serializer: ImageResponseSerializer? = nil,filter: ImageFilter? = nil,progress: ProgressHandler? = nil,progressQueue: DispatchQueue = DispatchQueue.main,completion: CompletionHandler? = nil)-> RequestReceipt? {var queuedRequest: DataRequest?synchronizationQueue.sync {// 1) Append the filter and completion handler to a pre-existing request if it already existslet urlID = ImageDownloader.urlIdentifier(for: urlRequest)if let responseHandler = self.responseHandlers[urlID] {responseHandler.operations.append((receiptID: receiptID, filter: filter, completion: completion))queuedRequest = responseHandler.requestreturn}// 2) Attempt to load the image from the image cache if the cache policy allows itif let nonNilURLRequest = urlRequest.urlRequest {switch nonNilURLRequest.cachePolicy {case .useProtocolCachePolicy, .returnCacheDataElseLoad, .returnCacheDataDontLoad:let cachedImage: Image?if let cacheKey = cacheKey {cachedImage = self.imageCache?.image(withIdentifier: cacheKey)} else {cachedImage = self.imageCache?.image(for: nonNilURLRequest, withIdentifier: filter?.identifier)}if let image = cachedImage {DispatchQueue.main.async {let response = AFIDataResponse<Image>(request: urlRequest.urlRequest,response: nil,data: nil,metrics: nil,serializationDuration: 0.0,result: .success(image))completion?(response)}return}default:break}}// 3) Create the request and set up authentication, validation and response serializationlet request = self.session.request(urlRequest)queuedRequest = requestif let credential = self.credential {request.authenticate(with: credential)}request.validate()if let progress = progress {request.downloadProgress(queue: progressQueue, closure: progress)}// Generate a unique handler id to check whether the active request has changed while downloadinglet handlerID = UUID().uuidStringrequest.response(queue: self.responseQueue,responseSerializer: serializer ?? imageResponseSerializer,completionHandler: { response indefer {self.safelyDecrementActiveRequestCount()self.safelyStartNextRequestIfNecessary()}// Early out if the request has changed out from under usguardlet handler = self.safelyFetchResponseHandler(withURLIdentifier: urlID),handler.handlerID == handlerID,let responseHandler = self.safelyRemoveResponseHandler(withURLIdentifier: urlID)else {return}switch response.result {case let .success(image):var filteredImages: [String: Image] = [:]for (_, filter, completion) in responseHandler.operations {var filteredImage: Imageif let filter = filter {if let alreadyFilteredImage = filteredImages[filter.identifier] {filteredImage = alreadyFilteredImage} else {filteredImage = filter.filter(image)filteredImages[filter.identifier] = filteredImage}} else {filteredImage = image}if let cacheKey = cacheKey {self.imageCache?.add(filteredImage, withIdentifier: cacheKey)} else if let request = response.request {self.imageCache?.add(filteredImage, for: request, withIdentifier: filter?.identifier)}DispatchQueue.main.async {let response = AFIDataResponse<Image>(request: response.request,response: response.response,data: response.data,metrics: response.metrics,serializationDuration: response.serializationDuration,result: .success(filteredImage))completion?(response)}}case .failure:for (_, _, completion) in responseHandler.operations {DispatchQueue.main.async { completion?(response.mapError { AFIError.alamofireError($0) }) }}}})// 4) Store the response handler for use when the request completeslet responseHandler = ResponseHandler(request: request,handlerID: handlerID,receiptID: receiptID,filter: filter,completion: completion)self.responseHandlers[urlID] = responseHandler// 5) Either start the request or enqueue it depending on the current active request countif self.isActiveRequestCountBelowMaximumLimit() {self.start(request)} else {self.enqueue(request)}}if let request = queuedRequest {return RequestReceipt(request: request, receiptID: receiptID)}return nil}/// Creates a download request using the internal Alamofire `SessionManager` instance for each specified URL request.////// For each request, if the same download request is already in the queue or currently being downloaded, the/// filter and completion handler are appended to the already existing request. Once the request completes, all/// filters and completion handlers attached to the request are executed in the order they were added./// Additionally, any filters attached to the request with the same identifiers are only executed once. The/// resulting image is then passed into each completion handler paired with the filter.////// You should not attempt to directly cancel any of the `request`s inside the request receipts array since other/// callers may be relying on the completion of that request. Instead, you should call/// `cancelRequestForRequestReceipt` with the returned request receipt to allow the `ImageDownloader` to optimize/// the cancellation on behalf of all active callers.////// - parameter urlRequests: The URL requests./// - parameter filter The image filter to apply to the image after each download is complete./// - parameter progress: The closure to be executed periodically during the lifecycle of the request. Defaults/// to `nil`./// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue./// - parameter completion: The closure called when each download request is complete.////// - returns: The request receipts for the download requests if available. If an image is stored in the image/// cache and the URL request cache policy allows the cache to be used, a receipt will not be returned/// for that request.@discardableResultopen func download(_ urlRequests: [URLRequestConvertible],filter: ImageFilter? = nil,progress: ProgressHandler? = nil,progressQueue: DispatchQueue = DispatchQueue.main,completion: CompletionHandler? = nil)-> [RequestReceipt] {urlRequests.compactMap {download($0, filter: filter, progress: progress, progressQueue: progressQueue, completion: completion)}}/// Cancels the request contained inside the receipt calls the completion handler with a request cancelled error.////// - Parameter requestReceipt: The request receipt to cancel.open func cancelRequest(with requestReceipt: RequestReceipt) {synchronizationQueue.sync {let urlID = ImageDownloader.urlIdentifier(for: requestReceipt.request.convertible)guard let responseHandler = self.responseHandlers[urlID] else { return }let index = responseHandler.operations.firstIndex { $0.receiptID == requestReceipt.receiptID }if let index = index {let operation = responseHandler.operations.remove(at: index)let response: AFIDataResponse<Image> = {let urlRequest = requestReceipt.request.requestlet error = AFIError.requestCancelledreturn DataResponse(request: urlRequest,response: nil,data: nil,metrics: nil,serializationDuration: 0.0,result: .failure(error))}()DispatchQueue.main.async { operation.completion?(response) }}if responseHandler.operations.isEmpty {requestReceipt.request.cancel()self.responseHandlers.removeValue(forKey: urlID)}}}// MARK: Internal - Thread-Safe Request Methodsfunc safelyFetchResponseHandler(withURLIdentifier urlIdentifier: String) -> ResponseHandler? {var responseHandler: ResponseHandler?synchronizationQueue.sync {responseHandler = self.responseHandlers[urlIdentifier]}return responseHandler}func safelyRemoveResponseHandler(withURLIdentifier identifier: String) -> ResponseHandler? {var responseHandler: ResponseHandler?synchronizationQueue.sync {responseHandler = self.responseHandlers.removeValue(forKey: identifier)}return responseHandler}func safelyStartNextRequestIfNecessary() {synchronizationQueue.sync {guard self.isActiveRequestCountBelowMaximumLimit() else { return }guard let request = self.dequeue() else { return }self.start(request)}}func safelyDecrementActiveRequestCount() {synchronizationQueue.sync {self.activeRequestCount -= 1}}// MARK: Internal - Non Thread-Safe Request Methodsfunc start(_ request: Request) {request.resume()activeRequestCount += 1}func enqueue(_ request: Request) {switch downloadPrioritization {case .fifo:queuedRequests.append(request)case .lifo:queuedRequests.insert(request, at: 0)}}@discardableResultfunc dequeue() -> Request? {var request: Request?if !queuedRequests.isEmpty {request = queuedRequests.removeFirst()}return request}func isActiveRequestCountBelowMaximumLimit() -> Bool {activeRequestCount < maximumActiveDownloads}static func urlIdentifier(for urlRequest: URLRequestConvertible) -> String {var urlID: String?do {urlID = try urlRequest.asURLRequest().url?.absoluteString} catch {// No-op}return urlID ?? ""}}