123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- // ImageDownloader.swift
- //
- // Copyright (c) 2015-2016 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 Alamofire
- import Foundation
- #if os(iOS) || os(tvOS) || os(watchOS)
- import UIKit
- #elseif os(OSX)
- import Cocoa
- #endif
- /// 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.
- public class RequestReceipt {
- /// The download request created by the `ImageDownloader`.
- public let request: Request
- /// The unique identifier for the image filters and completion handlers when duplicate requests are made.
- public let receiptID: String
- init(request: Request, receiptID: String) {
- self.request = request
- self.receiptID = receiptID
- }
- }
- /// 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.
- public class ImageDownloader {
- /// The completion handler closure used when an image download completes.
- public typealias CompletionHandler = Response<Image, NSError> -> Void
- /**
- 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
- }
- class ResponseHandler {
- let identifier: String
- let request: Request
- var operations: [(id: String, filter: ImageFilter?, completion: CompletionHandler?)]
- init(request: Request, id: String, filter: ImageFilter?, completion: CompletionHandler?) {
- self.request = request
- self.identifier = ImageDownloader.identifierForURLRequest(request.request!)
- self.operations = [(id: id, 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.
- public private(set) var credential: NSURLCredential?
- /// The underlying Alamofire `Manager` instance used to handle all download requests.
- public let sessionManager: Alamofire.Manager
- let downloadPrioritization: DownloadPrioritization
- let maximumActiveDownloads: Int
- var activeRequestCount = 0
- var queuedRequests: [Request] = []
- var responseHandlers: [String: ResponseHandler] = [:]
- private let synchronizationQueue: dispatch_queue_t = {
- let name = String(format: "com.alamofire.imagedownloader.synchronizationqueue-%08%08", arc4random(), arc4random())
- return dispatch_queue_create(name, DISPATCH_QUEUE_SERIAL)
- }()
- private let responseQueue: dispatch_queue_t = {
- let name = String(format: "com.alamofire.imagedownloader.responsequeue-%08%08", arc4random(), arc4random())
- return dispatch_queue_create(name, DISPATCH_QUEUE_CONCURRENT)
- }()
- // MARK: - Initialization
- /// The default instance of `ImageDownloader` initialized with default values.
- public static let defaultInstance = ImageDownloader()
- /**
- Creates a default `NSURLSessionConfiguration` with common usage parameter values.
-
- - returns: The default `NSURLSessionConfiguration` instance.
- */
- public class func defaultURLSessionConfiguration() -> NSURLSessionConfiguration {
- let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
- configuration.HTTPAdditionalHeaders = Manager.defaultHTTPHeaders
- configuration.HTTPShouldSetCookies = true
- configuration.HTTPShouldUsePipelining = false
- configuration.requestCachePolicy = .UseProtocolCachePolicy
- configuration.allowsCellularAccess = true
- configuration.timeoutIntervalForRequest = 60
- configuration.URLCache = ImageDownloader.defaultURLCache()
- return configuration
- }
- /**
- Creates a default `NSURLCache` with common usage parameter values.
- - returns: The default `NSURLCache` instance.
- */
- public class func defaultURLCache() -> NSURLCache {
- return NSURLCache(
- memoryCapacity: 20 * 1024 * 1024, // 20 MB
- diskCapacity: 150 * 1024 * 1024, // 150 MB
- diskPath: "com.alamofire.imagedownloader"
- )
- }
- /**
- Initializes the `ImageDownloader` instance with the given configuration, download prioritization, maximum active
- download count and image cache.
- - parameter configuration: The `NSURLSessionConfiguration` to use to create the underlying Alamofire
- `Manager` 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: NSURLSessionConfiguration = ImageDownloader.defaultURLSessionConfiguration(),
- downloadPrioritization: DownloadPrioritization = .FIFO,
- maximumActiveDownloads: Int = 4,
- imageCache: ImageRequestCache? = AutoPurgingImageCache())
- {
- self.sessionManager = Alamofire.Manager(configuration: configuration)
- self.sessionManager.startRequestsImmediately = false
- self.downloadPrioritization = downloadPrioritization
- self.maximumActiveDownloads = maximumActiveDownloads
- self.imageCache = imageCache
- }
- /**
- Initializes the `ImageDownloader` instance with the given sesion manager, download prioritization, maximum
- active download count and image cache.
- - parameter sessionManager: The Alamofire `Manager` 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(
- sessionManager: Manager,
- downloadPrioritization: DownloadPrioritization = .FIFO,
- maximumActiveDownloads: Int = 4,
- imageCache: ImageRequestCache? = AutoPurgingImageCache())
- {
- self.sessionManager = sessionManager
- self.sessionManager.startRequestsImmediately = false
- self.downloadPrioritization = downloadPrioritization
- self.maximumActiveDownloads = maximumActiveDownloads
- self.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.
- */
- public func addAuthentication(
- user user: String,
- password: String,
- persistence: NSURLCredentialPersistence = .ForSession)
- {
- let credential = NSURLCredential(user: user, password: password, persistence: persistence)
- addAuthentication(usingCredential: credential)
- }
- /**
- Associates the specified credential with all future download requests.
- - parameter credential: The credential.
- */
- public func addAuthentication(usingCredential credential: NSURLCredential) {
- dispatch_sync(synchronizationQueue) {
- self.credential = credential
- }
- }
- // MARK: - Download
- /**
- Creates a download request using the internal Alamofire `Manager` 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 filter The image filter to apply to the image after the download is complete. Defaults to `nil`.
- - parameter completion: The closure called when the download request is complete.
- - 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.
- */
- public func downloadImage(
- URLRequest URLRequest: URLRequestConvertible,
- filter: ImageFilter? = nil,
- completion: CompletionHandler? = nil)
- -> RequestReceipt?
- {
- return downloadImage(
- URLRequest: URLRequest,
- receiptID: NSUUID().UUIDString,
- filter: filter,
- completion: completion
- )
- }
- func downloadImage(
- URLRequest URLRequest: URLRequestConvertible,
- receiptID: String,
- filter: ImageFilter?,
- completion: CompletionHandler?)
- -> RequestReceipt?
- {
- var request: Request!
- dispatch_sync(synchronizationQueue) {
- // 1) Append the filter and completion handler to a pre-existing request if it already exists
- let identifier = ImageDownloader.identifierForURLRequest(URLRequest)
- if let responseHandler = self.responseHandlers[identifier] {
- responseHandler.operations.append(id: receiptID, filter: filter, completion: completion)
- request = responseHandler.request
- return
- }
- // 2) Attempt to load the image from the image cache if the cache policy allows it
- switch URLRequest.URLRequest.cachePolicy {
- case .UseProtocolCachePolicy, .ReturnCacheDataElseLoad, .ReturnCacheDataDontLoad:
- if let image = self.imageCache?.imageForRequest(
- URLRequest.URLRequest,
- withAdditionalIdentifier: filter?.identifier)
- {
- dispatch_async(dispatch_get_main_queue()) {
- let response = Response<Image, NSError>(
- request: URLRequest.URLRequest,
- response: nil,
- data: nil,
- result: .Success(image)
- )
- completion?(response)
- }
- return
- }
- default:
- break
- }
- // 3) Create the request and set up authentication, validation and response serialization
- request = self.sessionManager.request(URLRequest)
- if let credential = self.credential {
- request.authenticate(usingCredential: credential)
- }
- request.validate()
- request.response(
- queue: self.responseQueue,
- responseSerializer: Request.imageResponseSerializer(),
- completionHandler: { [weak self] response in
- guard let strongSelf = self, let request = response.request else { return }
- let responseHandler = strongSelf.safelyRemoveResponseHandlerWithIdentifier(identifier)
- switch response.result {
- case .Success(let image):
- var filteredImages: [String: Image] = [:]
- for (_, filter, completion) in responseHandler.operations {
- var filteredImage: Image
- if let filter = filter {
- if let alreadyFilteredImage = filteredImages[filter.identifier] {
- filteredImage = alreadyFilteredImage
- } else {
- filteredImage = filter.filter(image)
- filteredImages[filter.identifier] = filteredImage
- }
- } else {
- filteredImage = image
- }
- strongSelf.imageCache?.addImage(
- filteredImage,
- forRequest: request,
- withAdditionalIdentifier: filter?.identifier
- )
- dispatch_async(dispatch_get_main_queue()) {
- let response = Response<Image, NSError>(
- request: response.request,
- response: response.response,
- data: response.data,
- result: .Success(filteredImage),
- timeline: response.timeline
- )
- completion?(response)
- }
- }
- case .Failure:
- for (_, _, completion) in responseHandler.operations {
- dispatch_async(dispatch_get_main_queue()) { completion?(response) }
- }
- }
- strongSelf.safelyDecrementActiveRequestCount()
- strongSelf.safelyStartNextRequestIfNecessary()
- }
- )
- // 4) Store the response handler for use when the request completes
- let responseHandler = ResponseHandler(
- request: request,
- id: receiptID,
- filter: filter,
- completion: completion
- )
- self.responseHandlers[identifier] = responseHandler
- // 5) Either start the request or enqueue it depending on the current active request count
- if self.isActiveRequestCountBelowMaximumLimit() {
- self.startRequest(request)
- } else {
- self.enqueueRequest(request)
- }
- }
- if let request = request {
- return RequestReceipt(request: request, receiptID: receiptID)
- }
- return nil
- }
- /**
- Creates a download request using the internal Alamofire `Manager` 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 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.
- */
- public func downloadImages(
- URLRequests URLRequests: [URLRequestConvertible],
- filter: ImageFilter? = nil,
- completion: CompletionHandler? = nil)
- -> [RequestReceipt]
- {
- return URLRequests.flatMap { downloadImage(URLRequest: $0, filter: filter, completion: completion) }
- }
- /**
- Cancels the request in the receipt by removing the response handler and cancelling the request if necessary.
- If the request is pending in the queue, it will be cancelled if no other response handlers are registered with
- the request. If the request is currently executing or is already completed, the response handler is removed and
- will not be called.
- - parameter requestReceipt: The request receipt to cancel.
- */
- public func cancelRequestForRequestReceipt(requestReceipt: RequestReceipt) {
- dispatch_sync(synchronizationQueue) {
- let identifier = ImageDownloader.identifierForURLRequest(requestReceipt.request.request!)
- guard let responseHandler = self.responseHandlers[identifier] else { return }
- if let index = responseHandler.operations.indexOf({ $0.id == requestReceipt.receiptID }) {
- let operation = responseHandler.operations.removeAtIndex(index)
- let response: Response<Image, NSError> = {
- let URLRequest = requestReceipt.request.request!
- let error: NSError = {
- let failureReason = "ImageDownloader cancelled URL request: \(URLRequest.URLString)"
- let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
- return NSError(domain: Error.Domain, code: NSURLErrorCancelled, userInfo: userInfo)
- }()
- return Response(request: URLRequest, response: nil, data: nil, result: .Failure(error))
- }()
- dispatch_async(dispatch_get_main_queue()) { operation.completion?(response) }
- }
- if responseHandler.operations.isEmpty && requestReceipt.request.task.state == .Suspended {
- requestReceipt.request.cancel()
- }
- }
- }
- // MARK: - Internal - Thread-Safe Request Methods
- func safelyRemoveResponseHandlerWithIdentifier(identifier: String) -> ResponseHandler {
- var responseHandler: ResponseHandler!
- dispatch_sync(synchronizationQueue) {
- responseHandler = self.responseHandlers.removeValueForKey(identifier)
- }
- return responseHandler
- }
- func safelyStartNextRequestIfNecessary() {
- dispatch_sync(synchronizationQueue) {
- guard self.isActiveRequestCountBelowMaximumLimit() else { return }
- while (!self.queuedRequests.isEmpty) {
- if let request = self.dequeueRequest() where request.task.state == .Suspended {
- self.startRequest(request)
- break
- }
- }
- }
- }
- func safelyDecrementActiveRequestCount() {
- dispatch_sync(self.synchronizationQueue) {
- if self.activeRequestCount > 0 {
- self.activeRequestCount -= 1
- }
- }
- }
- // MARK: - Internal - Non Thread-Safe Request Methods
- func startRequest(request: Request) {
- request.resume()
- activeRequestCount += 1
- }
- func enqueueRequest(request: Request) {
- switch downloadPrioritization {
- case .FIFO:
- queuedRequests.append(request)
- case .LIFO:
- queuedRequests.insert(request, atIndex: 0)
- }
- }
- func dequeueRequest() -> Request? {
- var request: Request?
- if !queuedRequests.isEmpty {
- request = queuedRequests.removeFirst()
- }
- return request
- }
- func isActiveRequestCountBelowMaximumLimit() -> Bool {
- return activeRequestCount < maximumActiveDownloads
- }
- static func identifierForURLRequest(URLRequest: URLRequestConvertible) -> String {
- return URLRequest.URLRequest.URLString
- }
- }
|