1 |
efrain |
1 |
//
|
|
|
2 |
// ImageDownloader.swift
|
|
|
3 |
//
|
|
|
4 |
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
|
|
5 |
//
|
|
|
6 |
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
7 |
// of this software and associated documentation files (the "Software"), to deal
|
|
|
8 |
// in the Software without restriction, including without limitation the rights
|
|
|
9 |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
10 |
// copies of the Software, and to permit persons to whom the Software is
|
|
|
11 |
// furnished to do so, subject to the following conditions:
|
|
|
12 |
//
|
|
|
13 |
// The above copyright notice and this permission notice shall be included in
|
|
|
14 |
// all copies or substantial portions of the Software.
|
|
|
15 |
//
|
|
|
16 |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
17 |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
18 |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
19 |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
20 |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
21 |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
22 |
// THE SOFTWARE.
|
|
|
23 |
//
|
|
|
24 |
|
|
|
25 |
import Alamofire
|
|
|
26 |
import Foundation
|
|
|
27 |
|
|
|
28 |
#if os(iOS) || os(tvOS) || os(watchOS)
|
|
|
29 |
import UIKit
|
|
|
30 |
#elseif os(macOS)
|
|
|
31 |
import Cocoa
|
|
|
32 |
#endif
|
|
|
33 |
|
|
|
34 |
/// Alias for `DataResponse<T, AFIError>`.
|
|
|
35 |
public typealias AFIDataResponse<T> = DataResponse<T, AFIError>
|
|
|
36 |
|
|
|
37 |
/// Alias for `Result<T, AFIError>`.
|
|
|
38 |
public typealias AFIResult<T> = Result<T, AFIError>
|
|
|
39 |
|
|
|
40 |
/// The `RequestReceipt` is an object vended by the `ImageDownloader` when starting a download request. It can be used
|
|
|
41 |
/// to cancel active requests running on the `ImageDownloader` session. As a general rule, image download requests
|
|
|
42 |
/// should be cancelled using the `RequestReceipt` instead of calling `cancel` directly on the `request` itself. The
|
|
|
43 |
/// `ImageDownloader` is optimized to handle duplicate request scenarios as well as pending versus active downloads.
|
|
|
44 |
open class RequestReceipt {
|
|
|
45 |
/// The download request created by the `ImageDownloader`.
|
|
|
46 |
public let request: DataRequest
|
|
|
47 |
|
|
|
48 |
/// The unique identifier for the image filters and completion handlers when duplicate requests are made.
|
|
|
49 |
public let receiptID: String
|
|
|
50 |
|
|
|
51 |
init(request: DataRequest, receiptID: String) {
|
|
|
52 |
self.request = request
|
|
|
53 |
self.receiptID = receiptID
|
|
|
54 |
}
|
|
|
55 |
}
|
|
|
56 |
|
|
|
57 |
// MARK: -
|
|
|
58 |
|
|
|
59 |
/// The `ImageDownloader` class is responsible for downloading images in parallel on a prioritized queue. Incoming
|
|
|
60 |
/// downloads are added to the front or back of the queue depending on the download prioritization. Each downloaded
|
|
|
61 |
/// image is cached in the underlying `NSURLCache` as well as the in-memory image cache that supports image filters.
|
|
|
62 |
/// By default, any download request with a cached image equivalent in the image cache will automatically be served the
|
|
|
63 |
/// cached image representation. Additional advanced features include supporting multiple image filters and completion
|
|
|
64 |
/// handlers for a single request.
|
|
|
65 |
open class ImageDownloader {
|
|
|
66 |
/// The completion handler closure used when an image download completes.
|
|
|
67 |
public typealias CompletionHandler = (AFIDataResponse<Image>) -> Void
|
|
|
68 |
|
|
|
69 |
/// The progress handler closure called periodically during an image download.
|
|
|
70 |
public typealias ProgressHandler = DataRequest.ProgressHandler
|
|
|
71 |
|
|
|
72 |
// MARK: Helper Types
|
|
|
73 |
|
|
|
74 |
/// Defines the order prioritization of incoming download requests being inserted into the queue.
|
|
|
75 |
///
|
|
|
76 |
/// - fifo: All incoming downloads are added to the back of the queue.
|
|
|
77 |
/// - lifo: All incoming downloads are added to the front of the queue.
|
|
|
78 |
public enum DownloadPrioritization {
|
|
|
79 |
case fifo, lifo
|
|
|
80 |
}
|
|
|
81 |
|
|
|
82 |
final class ResponseHandler {
|
|
|
83 |
let urlID: String
|
|
|
84 |
let handlerID: String
|
|
|
85 |
let request: DataRequest
|
|
|
86 |
var operations: [(receiptID: String, filter: ImageFilter?, completion: CompletionHandler?)]
|
|
|
87 |
|
|
|
88 |
init(request: DataRequest,
|
|
|
89 |
handlerID: String,
|
|
|
90 |
receiptID: String,
|
|
|
91 |
filter: ImageFilter?,
|
|
|
92 |
completion: CompletionHandler?) {
|
|
|
93 |
self.request = request
|
|
|
94 |
urlID = ImageDownloader.urlIdentifier(for: request.convertible)
|
|
|
95 |
self.handlerID = handlerID
|
|
|
96 |
operations = [(receiptID: receiptID, filter: filter, completion: completion)]
|
|
|
97 |
}
|
|
|
98 |
}
|
|
|
99 |
|
|
|
100 |
// MARK: Properties
|
|
|
101 |
|
|
|
102 |
/// The image cache used to store all downloaded images in.
|
|
|
103 |
public let imageCache: ImageRequestCache?
|
|
|
104 |
|
|
|
105 |
/// The credential used for authenticating each download request.
|
|
|
106 |
open private(set) var credential: URLCredential?
|
|
|
107 |
|
|
|
108 |
/// Response serializer used to convert the image data to UIImage.
|
|
|
109 |
public var imageResponseSerializer = ImageResponseSerializer()
|
|
|
110 |
|
|
|
111 |
/// The underlying Alamofire `Session` instance used to handle all download requests.
|
|
|
112 |
public let session: Session
|
|
|
113 |
|
|
|
114 |
let downloadPrioritization: DownloadPrioritization
|
|
|
115 |
let maximumActiveDownloads: Int
|
|
|
116 |
|
|
|
117 |
var activeRequestCount = 0
|
|
|
118 |
var queuedRequests: [Request] = []
|
|
|
119 |
var responseHandlers: [String: ResponseHandler] = [:]
|
|
|
120 |
|
|
|
121 |
private let synchronizationQueue: DispatchQueue = {
|
|
|
122 |
let name = String(format: "org.alamofire.imagedownloader.synchronizationqueue-%08x%08x", arc4random(), arc4random())
|
|
|
123 |
return DispatchQueue(label: name)
|
|
|
124 |
}()
|
|
|
125 |
|
|
|
126 |
private let responseQueue: DispatchQueue = {
|
|
|
127 |
let name = String(format: "org.alamofire.imagedownloader.responsequeue-%08x%08x", arc4random(), arc4random())
|
|
|
128 |
return DispatchQueue(label: name, attributes: .concurrent)
|
|
|
129 |
}()
|
|
|
130 |
|
|
|
131 |
// MARK: Initialization
|
|
|
132 |
|
|
|
133 |
/// The default instance of `ImageDownloader` initialized with default values.
|
|
|
134 |
public static let `default` = ImageDownloader()
|
|
|
135 |
|
|
|
136 |
/// Creates a default `URLSessionConfiguration` with common usage parameter values.
|
|
|
137 |
///
|
|
|
138 |
/// - returns: The default `URLSessionConfiguration` instance.
|
|
|
139 |
open class func defaultURLSessionConfiguration() -> URLSessionConfiguration {
|
|
|
140 |
let configuration = URLSessionConfiguration.default
|
|
|
141 |
|
|
|
142 |
configuration.headers = .default
|
|
|
143 |
configuration.httpShouldSetCookies = true
|
|
|
144 |
configuration.httpShouldUsePipelining = false
|
|
|
145 |
|
|
|
146 |
configuration.requestCachePolicy = .useProtocolCachePolicy
|
|
|
147 |
configuration.allowsCellularAccess = true
|
|
|
148 |
configuration.timeoutIntervalForRequest = 60
|
|
|
149 |
|
|
|
150 |
configuration.urlCache = ImageDownloader.defaultURLCache()
|
|
|
151 |
|
|
|
152 |
return configuration
|
|
|
153 |
}
|
|
|
154 |
|
|
|
155 |
/// Creates a default `URLCache` with common usage parameter values.
|
|
|
156 |
///
|
|
|
157 |
/// - returns: The default `URLCache` instance.
|
|
|
158 |
open class func defaultURLCache() -> URLCache {
|
|
|
159 |
let memoryCapacity = 20 * 1024 * 1024
|
|
|
160 |
let diskCapacity = 150 * 1024 * 1024
|
|
|
161 |
let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
|
|
162 |
let imageDownloaderPath = "org.alamofire.imagedownloader"
|
|
|
163 |
|
|
|
164 |
#if targetEnvironment(macCatalyst)
|
|
|
165 |
return URLCache(memoryCapacity: memoryCapacity,
|
|
|
166 |
diskCapacity: diskCapacity,
|
|
|
167 |
directory: cacheDirectory?.appendingPathComponent(imageDownloaderPath))
|
|
|
168 |
#else
|
|
|
169 |
#if os(macOS)
|
|
|
170 |
return URLCache(memoryCapacity: memoryCapacity,
|
|
|
171 |
diskCapacity: diskCapacity,
|
|
|
172 |
diskPath: cacheDirectory?.appendingPathComponent(imageDownloaderPath).path)
|
|
|
173 |
#else
|
|
|
174 |
return URLCache(memoryCapacity: memoryCapacity,
|
|
|
175 |
diskCapacity: diskCapacity,
|
|
|
176 |
diskPath: imageDownloaderPath)
|
|
|
177 |
#endif
|
|
|
178 |
#endif
|
|
|
179 |
}
|
|
|
180 |
|
|
|
181 |
/// Initializes the `ImageDownloader` instance with the given configuration, download prioritization, maximum active
|
|
|
182 |
/// download count and image cache.
|
|
|
183 |
///
|
|
|
184 |
/// - parameter configuration: The `URLSessionConfiguration` to use to create the underlying Alamofire
|
|
|
185 |
/// `SessionManager` instance.
|
|
|
186 |
/// - parameter downloadPrioritization: The download prioritization of the download queue. `.fifo` by default.
|
|
|
187 |
/// - parameter maximumActiveDownloads: The maximum number of active downloads allowed at any given time.
|
|
|
188 |
/// - parameter imageCache: The image cache used to store all downloaded images in.
|
|
|
189 |
///
|
|
|
190 |
/// - returns: The new `ImageDownloader` instance.
|
|
|
191 |
public init(configuration: URLSessionConfiguration = ImageDownloader.defaultURLSessionConfiguration(),
|
|
|
192 |
downloadPrioritization: DownloadPrioritization = .fifo,
|
|
|
193 |
maximumActiveDownloads: Int = 4,
|
|
|
194 |
imageCache: ImageRequestCache? = AutoPurgingImageCache()) {
|
|
|
195 |
session = Session(configuration: configuration, startRequestsImmediately: false)
|
|
|
196 |
self.downloadPrioritization = downloadPrioritization
|
|
|
197 |
self.maximumActiveDownloads = maximumActiveDownloads
|
|
|
198 |
self.imageCache = imageCache
|
|
|
199 |
}
|
|
|
200 |
|
|
|
201 |
/// Initializes the `ImageDownloader` instance with the given session manager, download prioritization, maximum
|
|
|
202 |
/// active download count and image cache.
|
|
|
203 |
///
|
|
|
204 |
/// - parameter session: The Alamofire `Session` instance to handle all download requests.
|
|
|
205 |
/// - parameter downloadPrioritization: The download prioritization of the download queue. `.fifo` by default.
|
|
|
206 |
/// - parameter maximumActiveDownloads: The maximum number of active downloads allowed at any given time.
|
|
|
207 |
/// - parameter imageCache: The image cache used to store all downloaded images in.
|
|
|
208 |
///
|
|
|
209 |
/// - returns: The new `ImageDownloader` instance.
|
|
|
210 |
public init(session: Session,
|
|
|
211 |
downloadPrioritization: DownloadPrioritization = .fifo,
|
|
|
212 |
maximumActiveDownloads: Int = 4,
|
|
|
213 |
imageCache: ImageRequestCache? = AutoPurgingImageCache()) {
|
|
|
214 |
precondition(!session.startRequestsImmediately, "Session must set `startRequestsImmediately` to `false`.")
|
|
|
215 |
|
|
|
216 |
self.session = session
|
|
|
217 |
self.downloadPrioritization = downloadPrioritization
|
|
|
218 |
self.maximumActiveDownloads = maximumActiveDownloads
|
|
|
219 |
self.imageCache = imageCache
|
|
|
220 |
}
|
|
|
221 |
|
|
|
222 |
// MARK: Authentication
|
|
|
223 |
|
|
|
224 |
/// Associates an HTTP Basic Auth credential with all future download requests.
|
|
|
225 |
///
|
|
|
226 |
/// - parameter user: The user.
|
|
|
227 |
/// - parameter password: The password.
|
|
|
228 |
/// - parameter persistence: The URL credential persistence. `.forSession` by default.
|
|
|
229 |
open func addAuthentication(user: String,
|
|
|
230 |
password: String,
|
|
|
231 |
persistence: URLCredential.Persistence = .forSession) {
|
|
|
232 |
let credential = URLCredential(user: user, password: password, persistence: persistence)
|
|
|
233 |
addAuthentication(usingCredential: credential)
|
|
|
234 |
}
|
|
|
235 |
|
|
|
236 |
/// Associates the specified credential with all future download requests.
|
|
|
237 |
///
|
|
|
238 |
/// - parameter credential: The credential.
|
|
|
239 |
open func addAuthentication(usingCredential credential: URLCredential) {
|
|
|
240 |
synchronizationQueue.sync {
|
|
|
241 |
self.credential = credential
|
|
|
242 |
}
|
|
|
243 |
}
|
|
|
244 |
|
|
|
245 |
// MARK: Download
|
|
|
246 |
|
|
|
247 |
/// Creates a download request using the internal Alamofire `SessionManager` instance for the specified URL request.
|
|
|
248 |
///
|
|
|
249 |
/// If the same download request is already in the queue or currently being downloaded, the filter and completion
|
|
|
250 |
/// handler are appended to the already existing request. Once the request completes, all filters and completion
|
|
|
251 |
/// handlers attached to the request are executed in the order they were added. Additionally, any filters attached
|
|
|
252 |
/// to the request with the same identifiers are only executed once. The resulting image is then passed into each
|
|
|
253 |
/// completion handler paired with the filter.
|
|
|
254 |
///
|
|
|
255 |
/// You should not attempt to directly cancel the `request` inside the request receipt since other callers may be
|
|
|
256 |
/// relying on the completion of that request. Instead, you should call `cancelRequestForRequestReceipt` with the
|
|
|
257 |
/// returned request receipt to allow the `ImageDownloader` to optimize the cancellation on behalf of all active
|
|
|
258 |
/// callers.
|
|
|
259 |
///
|
|
|
260 |
/// - parameter urlRequest: The URL request.
|
|
|
261 |
/// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults to `nil`.
|
|
|
262 |
/// - parameter receiptID: The `identifier` for the `RequestReceipt` returned. Defaults to a new, randomly
|
|
|
263 |
/// generated UUID.
|
|
|
264 |
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`. Defaults
|
|
|
265 |
/// to `nil` which will fall back to the instance `imageResponseSerializer`.
|
|
|
266 |
/// - parameter filter: The image filter to apply to the image after the download is complete. Defaults
|
|
|
267 |
/// to `nil`.
|
|
|
268 |
/// - parameter progress: The closure to be executed periodically during the lifecycle of the request.
|
|
|
269 |
/// Defaults to `nil`.
|
|
|
270 |
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue.
|
|
|
271 |
/// - parameter completion: The closure called when the download request is complete. Defaults to `nil`.
|
|
|
272 |
///
|
|
|
273 |
/// - returns: The request receipt for the download request if available. `nil` if the image is stored in the image
|
|
|
274 |
/// cache and the URL request cache policy allows the cache to be used.
|
|
|
275 |
@discardableResult
|
|
|
276 |
open func download(_ urlRequest: URLRequestConvertible,
|
|
|
277 |
cacheKey: String? = nil,
|
|
|
278 |
receiptID: String = UUID().uuidString,
|
|
|
279 |
serializer: ImageResponseSerializer? = nil,
|
|
|
280 |
filter: ImageFilter? = nil,
|
|
|
281 |
progress: ProgressHandler? = nil,
|
|
|
282 |
progressQueue: DispatchQueue = DispatchQueue.main,
|
|
|
283 |
completion: CompletionHandler? = nil)
|
|
|
284 |
-> RequestReceipt? {
|
|
|
285 |
var queuedRequest: DataRequest?
|
|
|
286 |
|
|
|
287 |
synchronizationQueue.sync {
|
|
|
288 |
// 1) Append the filter and completion handler to a pre-existing request if it already exists
|
|
|
289 |
let urlID = ImageDownloader.urlIdentifier(for: urlRequest)
|
|
|
290 |
|
|
|
291 |
if let responseHandler = self.responseHandlers[urlID] {
|
|
|
292 |
responseHandler.operations.append((receiptID: receiptID, filter: filter, completion: completion))
|
|
|
293 |
queuedRequest = responseHandler.request
|
|
|
294 |
return
|
|
|
295 |
}
|
|
|
296 |
|
|
|
297 |
// 2) Attempt to load the image from the image cache if the cache policy allows it
|
|
|
298 |
if let nonNilURLRequest = urlRequest.urlRequest {
|
|
|
299 |
switch nonNilURLRequest.cachePolicy {
|
|
|
300 |
case .useProtocolCachePolicy, .returnCacheDataElseLoad, .returnCacheDataDontLoad:
|
|
|
301 |
let cachedImage: Image?
|
|
|
302 |
|
|
|
303 |
if let cacheKey = cacheKey {
|
|
|
304 |
cachedImage = self.imageCache?.image(withIdentifier: cacheKey)
|
|
|
305 |
} else {
|
|
|
306 |
cachedImage = self.imageCache?.image(for: nonNilURLRequest, withIdentifier: filter?.identifier)
|
|
|
307 |
}
|
|
|
308 |
|
|
|
309 |
if let image = cachedImage {
|
|
|
310 |
DispatchQueue.main.async {
|
|
|
311 |
let response = AFIDataResponse<Image>(request: urlRequest.urlRequest,
|
|
|
312 |
response: nil,
|
|
|
313 |
data: nil,
|
|
|
314 |
metrics: nil,
|
|
|
315 |
serializationDuration: 0.0,
|
|
|
316 |
result: .success(image))
|
|
|
317 |
|
|
|
318 |
completion?(response)
|
|
|
319 |
}
|
|
|
320 |
|
|
|
321 |
return
|
|
|
322 |
}
|
|
|
323 |
default:
|
|
|
324 |
break
|
|
|
325 |
}
|
|
|
326 |
}
|
|
|
327 |
|
|
|
328 |
// 3) Create the request and set up authentication, validation and response serialization
|
|
|
329 |
let request = self.session.request(urlRequest)
|
|
|
330 |
queuedRequest = request
|
|
|
331 |
|
|
|
332 |
if let credential = self.credential {
|
|
|
333 |
request.authenticate(with: credential)
|
|
|
334 |
}
|
|
|
335 |
|
|
|
336 |
request.validate()
|
|
|
337 |
|
|
|
338 |
if let progress = progress {
|
|
|
339 |
request.downloadProgress(queue: progressQueue, closure: progress)
|
|
|
340 |
}
|
|
|
341 |
|
|
|
342 |
// Generate a unique handler id to check whether the active request has changed while downloading
|
|
|
343 |
let handlerID = UUID().uuidString
|
|
|
344 |
|
|
|
345 |
request.response(queue: self.responseQueue,
|
|
|
346 |
responseSerializer: serializer ?? imageResponseSerializer,
|
|
|
347 |
completionHandler: { response in
|
|
|
348 |
defer {
|
|
|
349 |
self.safelyDecrementActiveRequestCount()
|
|
|
350 |
self.safelyStartNextRequestIfNecessary()
|
|
|
351 |
}
|
|
|
352 |
|
|
|
353 |
// Early out if the request has changed out from under us
|
|
|
354 |
guard
|
|
|
355 |
let handler = self.safelyFetchResponseHandler(withURLIdentifier: urlID),
|
|
|
356 |
handler.handlerID == handlerID,
|
|
|
357 |
let responseHandler = self.safelyRemoveResponseHandler(withURLIdentifier: urlID)
|
|
|
358 |
else {
|
|
|
359 |
return
|
|
|
360 |
}
|
|
|
361 |
|
|
|
362 |
switch response.result {
|
|
|
363 |
case let .success(image):
|
|
|
364 |
var filteredImages: [String: Image] = [:]
|
|
|
365 |
|
|
|
366 |
for (_, filter, completion) in responseHandler.operations {
|
|
|
367 |
var filteredImage: Image
|
|
|
368 |
|
|
|
369 |
if let filter = filter {
|
|
|
370 |
if let alreadyFilteredImage = filteredImages[filter.identifier] {
|
|
|
371 |
filteredImage = alreadyFilteredImage
|
|
|
372 |
} else {
|
|
|
373 |
filteredImage = filter.filter(image)
|
|
|
374 |
filteredImages[filter.identifier] = filteredImage
|
|
|
375 |
}
|
|
|
376 |
} else {
|
|
|
377 |
filteredImage = image
|
|
|
378 |
}
|
|
|
379 |
|
|
|
380 |
if let cacheKey = cacheKey {
|
|
|
381 |
self.imageCache?.add(filteredImage, withIdentifier: cacheKey)
|
|
|
382 |
} else if let request = response.request {
|
|
|
383 |
self.imageCache?.add(filteredImage, for: request, withIdentifier: filter?.identifier)
|
|
|
384 |
}
|
|
|
385 |
|
|
|
386 |
DispatchQueue.main.async {
|
|
|
387 |
let response = AFIDataResponse<Image>(request: response.request,
|
|
|
388 |
response: response.response,
|
|
|
389 |
data: response.data,
|
|
|
390 |
metrics: response.metrics,
|
|
|
391 |
serializationDuration: response.serializationDuration,
|
|
|
392 |
result: .success(filteredImage))
|
|
|
393 |
|
|
|
394 |
completion?(response)
|
|
|
395 |
}
|
|
|
396 |
}
|
|
|
397 |
case .failure:
|
|
|
398 |
for (_, _, completion) in responseHandler.operations {
|
|
|
399 |
DispatchQueue.main.async { completion?(response.mapError { AFIError.alamofireError($0) }) }
|
|
|
400 |
}
|
|
|
401 |
}
|
|
|
402 |
})
|
|
|
403 |
|
|
|
404 |
// 4) Store the response handler for use when the request completes
|
|
|
405 |
let responseHandler = ResponseHandler(request: request,
|
|
|
406 |
handlerID: handlerID,
|
|
|
407 |
receiptID: receiptID,
|
|
|
408 |
filter: filter,
|
|
|
409 |
completion: completion)
|
|
|
410 |
|
|
|
411 |
self.responseHandlers[urlID] = responseHandler
|
|
|
412 |
|
|
|
413 |
// 5) Either start the request or enqueue it depending on the current active request count
|
|
|
414 |
if self.isActiveRequestCountBelowMaximumLimit() {
|
|
|
415 |
self.start(request)
|
|
|
416 |
} else {
|
|
|
417 |
self.enqueue(request)
|
|
|
418 |
}
|
|
|
419 |
}
|
|
|
420 |
|
|
|
421 |
if let request = queuedRequest {
|
|
|
422 |
return RequestReceipt(request: request, receiptID: receiptID)
|
|
|
423 |
}
|
|
|
424 |
|
|
|
425 |
return nil
|
|
|
426 |
}
|
|
|
427 |
|
|
|
428 |
/// Creates a download request using the internal Alamofire `SessionManager` instance for each specified URL request.
|
|
|
429 |
///
|
|
|
430 |
/// For each request, if the same download request is already in the queue or currently being downloaded, the
|
|
|
431 |
/// filter and completion handler are appended to the already existing request. Once the request completes, all
|
|
|
432 |
/// filters and completion handlers attached to the request are executed in the order they were added.
|
|
|
433 |
/// Additionally, any filters attached to the request with the same identifiers are only executed once. The
|
|
|
434 |
/// resulting image is then passed into each completion handler paired with the filter.
|
|
|
435 |
///
|
|
|
436 |
/// You should not attempt to directly cancel any of the `request`s inside the request receipts array since other
|
|
|
437 |
/// callers may be relying on the completion of that request. Instead, you should call
|
|
|
438 |
/// `cancelRequestForRequestReceipt` with the returned request receipt to allow the `ImageDownloader` to optimize
|
|
|
439 |
/// the cancellation on behalf of all active callers.
|
|
|
440 |
///
|
|
|
441 |
/// - parameter urlRequests: The URL requests.
|
|
|
442 |
/// - parameter filter The image filter to apply to the image after each download is complete.
|
|
|
443 |
/// - parameter progress: The closure to be executed periodically during the lifecycle of the request. Defaults
|
|
|
444 |
/// to `nil`.
|
|
|
445 |
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue.
|
|
|
446 |
/// - parameter completion: The closure called when each download request is complete.
|
|
|
447 |
///
|
|
|
448 |
/// - returns: The request receipts for the download requests if available. If an image is stored in the image
|
|
|
449 |
/// cache and the URL request cache policy allows the cache to be used, a receipt will not be returned
|
|
|
450 |
/// for that request.
|
|
|
451 |
@discardableResult
|
|
|
452 |
open func download(_ urlRequests: [URLRequestConvertible],
|
|
|
453 |
filter: ImageFilter? = nil,
|
|
|
454 |
progress: ProgressHandler? = nil,
|
|
|
455 |
progressQueue: DispatchQueue = DispatchQueue.main,
|
|
|
456 |
completion: CompletionHandler? = nil)
|
|
|
457 |
-> [RequestReceipt] {
|
|
|
458 |
urlRequests.compactMap {
|
|
|
459 |
download($0, filter: filter, progress: progress, progressQueue: progressQueue, completion: completion)
|
|
|
460 |
}
|
|
|
461 |
}
|
|
|
462 |
|
|
|
463 |
/// Cancels the request contained inside the receipt calls the completion handler with a request cancelled error.
|
|
|
464 |
///
|
|
|
465 |
/// - Parameter requestReceipt: The request receipt to cancel.
|
|
|
466 |
open func cancelRequest(with requestReceipt: RequestReceipt) {
|
|
|
467 |
synchronizationQueue.sync {
|
|
|
468 |
let urlID = ImageDownloader.urlIdentifier(for: requestReceipt.request.convertible)
|
|
|
469 |
guard let responseHandler = self.responseHandlers[urlID] else { return }
|
|
|
470 |
|
|
|
471 |
let index = responseHandler.operations.firstIndex { $0.receiptID == requestReceipt.receiptID }
|
|
|
472 |
|
|
|
473 |
if let index = index {
|
|
|
474 |
let operation = responseHandler.operations.remove(at: index)
|
|
|
475 |
|
|
|
476 |
let response: AFIDataResponse<Image> = {
|
|
|
477 |
let urlRequest = requestReceipt.request.request
|
|
|
478 |
let error = AFIError.requestCancelled
|
|
|
479 |
|
|
|
480 |
return DataResponse(request: urlRequest,
|
|
|
481 |
response: nil,
|
|
|
482 |
data: nil,
|
|
|
483 |
metrics: nil,
|
|
|
484 |
serializationDuration: 0.0,
|
|
|
485 |
result: .failure(error))
|
|
|
486 |
}()
|
|
|
487 |
|
|
|
488 |
DispatchQueue.main.async { operation.completion?(response) }
|
|
|
489 |
}
|
|
|
490 |
|
|
|
491 |
if responseHandler.operations.isEmpty {
|
|
|
492 |
requestReceipt.request.cancel()
|
|
|
493 |
self.responseHandlers.removeValue(forKey: urlID)
|
|
|
494 |
}
|
|
|
495 |
}
|
|
|
496 |
}
|
|
|
497 |
|
|
|
498 |
// MARK: Internal - Thread-Safe Request Methods
|
|
|
499 |
|
|
|
500 |
func safelyFetchResponseHandler(withURLIdentifier urlIdentifier: String) -> ResponseHandler? {
|
|
|
501 |
var responseHandler: ResponseHandler?
|
|
|
502 |
|
|
|
503 |
synchronizationQueue.sync {
|
|
|
504 |
responseHandler = self.responseHandlers[urlIdentifier]
|
|
|
505 |
}
|
|
|
506 |
|
|
|
507 |
return responseHandler
|
|
|
508 |
}
|
|
|
509 |
|
|
|
510 |
func safelyRemoveResponseHandler(withURLIdentifier identifier: String) -> ResponseHandler? {
|
|
|
511 |
var responseHandler: ResponseHandler?
|
|
|
512 |
|
|
|
513 |
synchronizationQueue.sync {
|
|
|
514 |
responseHandler = self.responseHandlers.removeValue(forKey: identifier)
|
|
|
515 |
}
|
|
|
516 |
|
|
|
517 |
return responseHandler
|
|
|
518 |
}
|
|
|
519 |
|
|
|
520 |
func safelyStartNextRequestIfNecessary() {
|
|
|
521 |
synchronizationQueue.sync {
|
|
|
522 |
guard self.isActiveRequestCountBelowMaximumLimit() else { return }
|
|
|
523 |
|
|
|
524 |
guard let request = self.dequeue() else { return }
|
|
|
525 |
|
|
|
526 |
self.start(request)
|
|
|
527 |
}
|
|
|
528 |
}
|
|
|
529 |
|
|
|
530 |
func safelyDecrementActiveRequestCount() {
|
|
|
531 |
synchronizationQueue.sync {
|
|
|
532 |
self.activeRequestCount -= 1
|
|
|
533 |
}
|
|
|
534 |
}
|
|
|
535 |
|
|
|
536 |
// MARK: Internal - Non Thread-Safe Request Methods
|
|
|
537 |
|
|
|
538 |
func start(_ request: Request) {
|
|
|
539 |
request.resume()
|
|
|
540 |
activeRequestCount += 1
|
|
|
541 |
}
|
|
|
542 |
|
|
|
543 |
func enqueue(_ request: Request) {
|
|
|
544 |
switch downloadPrioritization {
|
|
|
545 |
case .fifo:
|
|
|
546 |
queuedRequests.append(request)
|
|
|
547 |
case .lifo:
|
|
|
548 |
queuedRequests.insert(request, at: 0)
|
|
|
549 |
}
|
|
|
550 |
}
|
|
|
551 |
|
|
|
552 |
@discardableResult
|
|
|
553 |
func dequeue() -> Request? {
|
|
|
554 |
var request: Request?
|
|
|
555 |
|
|
|
556 |
if !queuedRequests.isEmpty {
|
|
|
557 |
request = queuedRequests.removeFirst()
|
|
|
558 |
}
|
|
|
559 |
|
|
|
560 |
return request
|
|
|
561 |
}
|
|
|
562 |
|
|
|
563 |
func isActiveRequestCountBelowMaximumLimit() -> Bool {
|
|
|
564 |
activeRequestCount < maximumActiveDownloads
|
|
|
565 |
}
|
|
|
566 |
|
|
|
567 |
static func urlIdentifier(for urlRequest: URLRequestConvertible) -> String {
|
|
|
568 |
var urlID: String?
|
|
|
569 |
|
|
|
570 |
do {
|
|
|
571 |
urlID = try urlRequest.asURLRequest().url?.absoluteString
|
|
|
572 |
} catch {
|
|
|
573 |
// No-op
|
|
|
574 |
}
|
|
|
575 |
|
|
|
576 |
return urlID ?? ""
|
|
|
577 |
}
|
|
|
578 |
}
|