Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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
}