|
|
@@ -32,121 +32,236 @@
|
|
|
#endif
|
|
|
|
|
|
|
|
|
-/// Progress update block of prefetcher.
|
|
|
-public typealias PrefetchProgressBlock = ((completedURLs: Int, allURLs: Int) -> ())
|
|
|
+/// Progress update block of prefetcher.
|
|
|
+///
|
|
|
+/// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
|
|
|
+/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while downloading, encountered an error when downloading or the download not being started at all.
|
|
|
+/// - `completedResources`: An array of resources that are downloaded and cached successfully.
|
|
|
+public typealias PrefetcherProgressBlock = ((skippedResources: [Resource], failedResources: [Resource], completedResources: [Resource]) -> ())
|
|
|
|
|
|
/// Completion block of prefetcher.
|
|
|
-public typealias PrefetchCompletionBlock = ((cancelled: Bool, completedURLs: Int, skippedURLs: Int) -> ())
|
|
|
+///
|
|
|
+/// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
|
|
|
+/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while downloading, encountered an error when downloading or the download not being started at all.
|
|
|
+/// - `completedResources`: An array of resources that are downloaded and cached successfully.
|
|
|
+public typealias PrefetcherCompletionHandler = ((skippedResources: [Resource], failedResources: [Resource], completedResources: [Resource]) -> ())
|
|
|
|
|
|
-private let defaultPrefetcherInstance = ImagePrefetcher()
|
|
|
-
|
|
|
-/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs and caching them.
|
|
|
-public class ImagePrefetcher: NSObject {
|
|
|
+/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them.
|
|
|
+/// This is useful when you know a list of image resources and want to download them before showing.
|
|
|
+public class ImagePrefetcher {
|
|
|
+
|
|
|
+ /// The maximum concurrent downloads to use when prefetching images. Default is 5.
|
|
|
+ public var maxConcurrentDownloads = 5
|
|
|
+
|
|
|
+ private let prefetchResources: [Resource]
|
|
|
+ private let optionsInfo: KingfisherOptionsInfo
|
|
|
+ private var progressBlock: PrefetcherProgressBlock?
|
|
|
+ private var completionHandler: PrefetcherCompletionHandler?
|
|
|
+
|
|
|
+ private var tasks = [NSURL: RetrieveImageDownloadTask]()
|
|
|
+
|
|
|
+ private var skippedResources = [Resource]()
|
|
|
+ private var completedResources = [Resource]()
|
|
|
+ private var failedResources = [Resource]()
|
|
|
|
|
|
- private var prefetchURLs: [NSURL]?
|
|
|
- private var skippedCount = 0
|
|
|
private var requestedCount = 0
|
|
|
- private var finishedCount = 0
|
|
|
+ private var cancelled = false
|
|
|
|
|
|
- private var cancelCompletionHandlerCalled = false
|
|
|
+ // The created manager used for prefetch. We will use the helper method in manager.
|
|
|
+ private let manager: KingfisherManager
|
|
|
|
|
|
- /// The default manager to use for downloads.
|
|
|
- public lazy var manager: KingfisherManager = KingfisherManager.sharedManager
|
|
|
-
|
|
|
- /// The default prefetcher.
|
|
|
- public class var defaultPrefetcher: ImagePrefetcher {
|
|
|
- return defaultPrefetcherInstance
|
|
|
+ private var finished: Bool {
|
|
|
+ return failedResources.count + skippedResources.count + completedResources.count == prefetchResources.count
|
|
|
}
|
|
|
-
|
|
|
- /// The maximum concurrent downloads to use when prefetching images. Default is 5.
|
|
|
- public var maxConcurrentDownloads = 5
|
|
|
|
|
|
/**
|
|
|
- Download the images from `urls` and cache them. This can be useful for background downloading
|
|
|
- of assets that are required for later use in an app. This code will not try and update any UI
|
|
|
- with the results of the process, but calls the handlers with the number cached etc. Failed
|
|
|
- images are just skipped.
|
|
|
+ Init an image prefetcher with an array of URLs.
|
|
|
|
|
|
- Warning: This will cancel any existing prefetch operation in progress! Use `isPrefetching() to
|
|
|
- control this in your own code as you see fit.
|
|
|
+ The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.
|
|
|
+ After you get a valid `ImagePrefetcher` object, you could call `start()` on it to begin the prefetching process.
|
|
|
+ The images already cached will be skipped without downloading again.
|
|
|
|
|
|
- - parameter urls: The list of URLs to prefetch
|
|
|
- - parameter progressBlock: Block to be called when progress updates. Completed and total
|
|
|
- counts are provided. Completed does not imply success.
|
|
|
- - parameter completionHandler: Block to be called when prefetching is complete. Completed is all
|
|
|
- those made, and skipped is the number of failed ones.
|
|
|
+ - parameter urls: The URLs which should be prefetched.
|
|
|
+ - parameter optionsInfo: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
|
|
|
+ - parameter progressBlock: Called every time an resource is downloaded, skipped or cancelled.
|
|
|
+ - parameter completionHandler: Called when the whole prefetching process finished.
|
|
|
+
|
|
|
+ - returns: An `ImagePrefetcher` object.
|
|
|
+
|
|
|
+ - Note: By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
|
|
|
+ the downloader and cache target respectively. You can specify another downloader or cache by using a customized `KingfisherOptionsInfo`.
|
|
|
+ Both the progress and completion block will be invoked in main thread. The `CallbackDispatchQueue` in `optionsInfo` will be ignored in this method.
|
|
|
*/
|
|
|
- public func prefetchURLs(urls: [NSURL], progressBlock: PrefetchProgressBlock?, completionHandler: PrefetchCompletionBlock?) {
|
|
|
-
|
|
|
- // Clear out any existing prefetch operation first
|
|
|
- cancelPrefetching()
|
|
|
-
|
|
|
- cancelCompletionHandlerCalled = false
|
|
|
+ public convenience init(urls: [NSURL],
|
|
|
+ optionsInfo: KingfisherOptionsInfo? = nil,
|
|
|
+ progressBlock: PrefetcherProgressBlock? = nil,
|
|
|
+ completionHandler: PrefetcherCompletionHandler? = nil)
|
|
|
+ {
|
|
|
+ let resources = urls.map { Resource(downloadURL: $0) }
|
|
|
+ self.init(resources: resources, optionsInfo: optionsInfo, progressBlock: progressBlock, completionHandler: completionHandler)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ Init an image prefetcher with an array of resources.
|
|
|
+
|
|
|
+ The prefetcher should be initiated with a list of prefetching targets. The resources list is immutable.
|
|
|
+ After you get a valid `ImagePrefetcher` object, you could call `start()` on it to begin the prefetching process.
|
|
|
+ The images already cached will be skipped without downloading again.
|
|
|
+
|
|
|
+ - parameter resources: The resources which should be prefetched. See `Resource` type for more.
|
|
|
+ - parameter optionsInfo: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
|
|
|
+ - parameter progressBlock: Called every time an resource is downloaded, skipped or cancelled.
|
|
|
+ - parameter completionHandler: Called when the whole prefetching process finished.
|
|
|
+
|
|
|
+ - returns: An `ImagePrefetcher` object.
|
|
|
+
|
|
|
+ - Note: By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
|
|
|
+ the downloader and cache target respectively. You can specify another downloader or cache by using a customized `KingfisherOptionsInfo`.
|
|
|
+ Both the progress and completion block will be invoked in main thread. The `CallbackDispatchQueue` in `optionsInfo` will be ignored in this method.
|
|
|
+ */
|
|
|
+ public init(resources: [Resource],
|
|
|
+ optionsInfo: KingfisherOptionsInfo? = nil,
|
|
|
+ progressBlock: PrefetcherProgressBlock? = nil,
|
|
|
+ completionHandler: PrefetcherCompletionHandler? = nil)
|
|
|
+ {
|
|
|
+ prefetchResources = resources
|
|
|
|
|
|
- prefetchURLs = urls
|
|
|
+ // We want all callbacks from main queue, so we ignore the call back queue in options
|
|
|
+ let optionsInfoWithoutQueue = optionsInfo?.kf_removeAllMatchesIgnoringAssociatedValue(.CallbackDispatchQueue(nil))
|
|
|
+ self.optionsInfo = optionsInfoWithoutQueue ?? KingfisherEmptyOptionsInfo
|
|
|
|
|
|
- guard urls.count > 0 else {
|
|
|
- CompletionHandler?()
|
|
|
- return
|
|
|
- }
|
|
|
+ let cache = self.optionsInfo.targetCache ?? ImageCache.defaultCache
|
|
|
+ let downloader = self.optionsInfo.downloader ?? ImageDownloader.defaultDownloader
|
|
|
+ manager = KingfisherManager(downloader: downloader, cache: cache)
|
|
|
|
|
|
- for i in (0..<urls.count) where i < maxConcurrentDownloads && requestedCount < urls.count {
|
|
|
- startPrefetching(i, progressBlock: progressBlock, completionHandler: completionHandler)
|
|
|
- }
|
|
|
+ self.progressBlock = progressBlock
|
|
|
+ self.completionHandler = completionHandler
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
- This cancels any existing prefetching activity that might be occuring. It does not stop any currently
|
|
|
- running cache operation, but prevents any further ones being started and terminates the looping. For
|
|
|
- surety, be sure that the completion block on the prefetch is called after calling this if you expect
|
|
|
- an operation to be running.
|
|
|
+ Start to download the resources and cache them. This can be useful for background downloading
|
|
|
+ of assets that are required for later use in an app. This code will not try and update any UI
|
|
|
+ with the results of the process.
|
|
|
*/
|
|
|
- func cancelPrefetching() {
|
|
|
- prefetchURLs = .None
|
|
|
- skippedCount = 0
|
|
|
- requestedCount = 0
|
|
|
- finishedCount = 0
|
|
|
+ public func start()
|
|
|
+ {
|
|
|
+ // Since we want to handle the resources cancellation in main thread only.
|
|
|
+ dispatch_async_safely_to_main_queue { () -> () in
|
|
|
+
|
|
|
+ guard !self.cancelled else {
|
|
|
+ assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.")
|
|
|
+ self.handleComplete()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ guard self.maxConcurrentDownloads > 0 else {
|
|
|
+ assertionFailure("There should be concurrent downloads value should be at least 1.")
|
|
|
+ self.handleComplete()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ guard self.prefetchResources.count > 0 else {
|
|
|
+ self.handleComplete()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let initialConcurentDownloads = min(self.prefetchResources.count, self.maxConcurrentDownloads)
|
|
|
+ for i in 0 ..< initialConcurentDownloads {
|
|
|
+ self.startPrefetchingResource(self.prefetchResources[i])
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+
|
|
|
/**
|
|
|
- Checks to see if this prefetcher is already prefetching any images.
|
|
|
-
|
|
|
- - returns: True if there are images still to be prefetched, false otherwise.
|
|
|
+ Stop current downloading progress, and cancel any future prefetching activity that might be occuring.
|
|
|
*/
|
|
|
- func isPrefetching() -> Bool {
|
|
|
- guard let urls = prefetchURLs else { return false }
|
|
|
- return urls.count > 0
|
|
|
+ public func cancel() {
|
|
|
+ dispatch_async_safely_to_main_queue {
|
|
|
+ self.cancelled = true
|
|
|
+ self.tasks.forEach { (_, task) -> () in
|
|
|
+ task.cancel()
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- internal func startPrefetching(index: Int, progressBlock: PrefetchProgressBlock?, completionHandler: PrefetchCompletionBlock?) {
|
|
|
- guard let urls = prefetchURLs where index < (urls.count ?? 0) else { return }
|
|
|
-
|
|
|
- requestedCount++
|
|
|
-
|
|
|
+ func downloadAndCacheResource(resource: Resource) {
|
|
|
+
|
|
|
let task = RetrieveImageTask()
|
|
|
- let resource = Resource(downloadURL: urls[index])
|
|
|
- let total = urls.count
|
|
|
+ let downloadTask = manager.downloadAndCacheImageWithURL(
|
|
|
+ resource.downloadURL,
|
|
|
+ forKey: resource.cacheKey,
|
|
|
+ retrieveImageTask: task,
|
|
|
+ progressBlock: nil,
|
|
|
+ completionHandler: {
|
|
|
+ (image, error, _, _) -> () in
|
|
|
+
|
|
|
+ self.tasks.removeValueForKey(resource.downloadURL)
|
|
|
+
|
|
|
+ if let _ = error {
|
|
|
+ self.failedResources.append(resource)
|
|
|
+ } else {
|
|
|
+ self.completedResources.append(resource)
|
|
|
+ }
|
|
|
+
|
|
|
+ self.reportProgress()
|
|
|
+
|
|
|
+ if self.cancelled {
|
|
|
+ if self.tasks.isEmpty {
|
|
|
+ let pendingResources = self.prefetchResources[self.requestedCount..<self.prefetchResources.count]
|
|
|
+ self.failedResources += Array(pendingResources)
|
|
|
+ self.handleComplete()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ self.reportCompletionOrStartNext()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ options: optionsInfo)
|
|
|
|
|
|
- manager.downloadAndCacheImageWithURL(resource.downloadURL, forKey: resource.cacheKey, retrieveImageTask: task, progressBlock: nil, completionHandler: { image, error, cacheType, imageURL in
|
|
|
- self.finishedCount++
|
|
|
-
|
|
|
- if image == .None {
|
|
|
- self.skippedCount++
|
|
|
+ if let downloadTask = downloadTask {
|
|
|
+ tasks[resource.downloadURL] = downloadTask
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func appendCachedResource(resource: Resource) {
|
|
|
+ skippedResources.append(resource)
|
|
|
+
|
|
|
+ reportProgress()
|
|
|
+ reportCompletionOrStartNext()
|
|
|
+ }
|
|
|
+
|
|
|
+ func startPrefetchingResource(resource: Resource)
|
|
|
+ {
|
|
|
+ requestedCount += 1
|
|
|
+ if optionsInfo.forceRefresh {
|
|
|
+ downloadAndCacheResource(resource)
|
|
|
+ } else {
|
|
|
+ let alreadyInCache = manager.cache.isImageCachedForKey(resource.cacheKey).cached
|
|
|
+ if alreadyInCache {
|
|
|
+ appendCachedResource(resource)
|
|
|
+ } else {
|
|
|
+ downloadAndCacheResource(resource)
|
|
|
}
|
|
|
-
|
|
|
- progressBlock?(completedURLs: self.finishedCount, allURLs: total)
|
|
|
-
|
|
|
- // Reference the prefetchURLs rather than urls in case the request has been cancelled
|
|
|
- if (self.prefetchURLs?.count ?? 0) > self.requestedCount {
|
|
|
- self.startPrefetching(self.requestedCount, progressBlock: progressBlock, completionHandler: completionHandler)
|
|
|
- } else if self.finishedCount == self.requestedCount {
|
|
|
- self.prefetchURLs?.removeAll()
|
|
|
- completionHandler?(cancelled: false, completedURLs: self.finishedCount, skippedURLs: self.skippedCount)
|
|
|
- } else if (self.prefetchURLs == nil || self.prefetchURLs!.count == 0) && !self.cancelCompletionHandlerCalled {
|
|
|
- self.cancelCompletionHandlerCalled = true
|
|
|
- completionHandler?(cancelled: true, completedURLs: self.finishedCount, skippedURLs: self.skippedCount)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func reportProgress() {
|
|
|
+ progressBlock?(skippedResources: skippedResources, failedResources: failedResources, completedResources: completedResources)
|
|
|
+ }
|
|
|
+
|
|
|
+ func reportCompletionOrStartNext() {
|
|
|
+ if finished {
|
|
|
+ handleComplete()
|
|
|
+ } else {
|
|
|
+ if requestedCount < prefetchResources.count {
|
|
|
+ startPrefetchingResource(prefetchResources[requestedCount])
|
|
|
}
|
|
|
-
|
|
|
- }, options: nil)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func handleComplete() {
|
|
|
+ completionHandler?(skippedResources: skippedResources, failedResources: failedResources, completedResources: completedResources)
|
|
|
+ completionHandler = nil
|
|
|
+ progressBlock = nil
|
|
|
}
|
|
|
}
|