You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
298 lines
14 KiB
298 lines
14 KiB
![]()
2 years ago
|
//
|
||
|
// KingfisherManager.swift
|
||
|
// Kingfisher
|
||
|
//
|
||
|
// Created by Wei Wang on 15/4/6.
|
||
|
//
|
||
|
// Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
|
||
|
//
|
||
|
// 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.
|
||
|
|
||
|
#if os(macOS)
|
||
|
import AppKit
|
||
|
#else
|
||
|
import UIKit
|
||
|
#endif
|
||
|
|
||
|
public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void)
|
||
|
public typealias CompletionHandler = ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> Void)
|
||
|
|
||
|
/// RetrieveImageTask represents a task of image retrieving process.
|
||
|
/// It contains an async task of getting image from disk and from network.
|
||
|
public final class RetrieveImageTask {
|
||
|
|
||
|
public static let empty = RetrieveImageTask()
|
||
|
|
||
|
// If task is canceled before the download task started (which means the `downloadTask` is nil),
|
||
|
// the download task should not begin.
|
||
|
var cancelledBeforeDownloadStarting: Bool = false
|
||
|
|
||
|
/// The network retrieve task in this image task.
|
||
|
public var downloadTask: RetrieveImageDownloadTask?
|
||
|
|
||
|
/**
|
||
|
Cancel current task. If this task is already done, do nothing.
|
||
|
*/
|
||
|
public func cancel() {
|
||
|
if let downloadTask = downloadTask {
|
||
|
downloadTask.cancel()
|
||
|
} else {
|
||
|
cancelledBeforeDownloadStarting = true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Error domain of Kingfisher
|
||
|
public let KingfisherErrorDomain = "com.onevcat.Kingfisher.Error"
|
||
|
|
||
|
/// Main manager class of Kingfisher. It connects Kingfisher downloader and cache.
|
||
|
/// You can use this class to retrieve an image via a specified URL from web or cache.
|
||
|
public class KingfisherManager {
|
||
|
|
||
|
/// Shared manager used by the extensions across Kingfisher.
|
||
|
public static let shared = KingfisherManager()
|
||
|
|
||
|
/// Cache used by this manager
|
||
|
public var cache: ImageCache
|
||
|
|
||
|
/// Downloader used by this manager
|
||
|
public var downloader: ImageDownloader
|
||
|
|
||
|
/// Default options used by the manager. This option will be used in
|
||
|
/// Kingfisher manager related methods, including all image view and
|
||
|
/// button extension methods. You can also passing the options per image by
|
||
|
/// sending an `options` parameter to Kingfisher's APIs, the per image option
|
||
|
/// will overwrite the default ones if exist.
|
||
|
///
|
||
|
/// - Note: This option will not be applied to independent using of `ImageDownloader` or `ImageCache`.
|
||
|
public var defaultOptions = KingfisherEmptyOptionsInfo
|
||
|
|
||
|
var currentDefaultOptions: KingfisherOptionsInfo {
|
||
|
return [.downloader(downloader), .targetCache(cache)] + defaultOptions
|
||
|
}
|
||
|
|
||
|
fileprivate let processQueue: DispatchQueue
|
||
|
|
||
|
convenience init() {
|
||
|
self.init(downloader: .default, cache: .default)
|
||
|
}
|
||
|
|
||
|
init(downloader: ImageDownloader, cache: ImageCache) {
|
||
|
self.downloader = downloader
|
||
|
self.cache = cache
|
||
|
|
||
|
let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)"
|
||
|
processQueue = DispatchQueue(label: processQueueName, attributes: .concurrent)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
Get an image with resource.
|
||
|
If KingfisherOptions.None is used as `options`, Kingfisher will seek the image in memory and disk first.
|
||
|
If not found, it will download the image at `resource.downloadURL` and cache it with `resource.cacheKey`.
|
||
|
These default behaviors could be adjusted by passing different options. See `KingfisherOptions` for more.
|
||
|
|
||
|
- parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`.
|
||
|
- parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
|
||
|
- parameter progressBlock: Called every time downloaded data changed. This could be used as a progress UI.
|
||
|
- parameter completionHandler: Called when the whole retrieving process finished.
|
||
|
|
||
|
- returns: A `RetrieveImageTask` task object. You can use this object to cancel the task.
|
||
|
*/
|
||
|
@discardableResult
|
||
|
public func retrieveImage(with resource: Resource,
|
||
|
options: KingfisherOptionsInfo?,
|
||
|
progressBlock: DownloadProgressBlock?,
|
||
|
completionHandler: CompletionHandler?) -> RetrieveImageTask
|
||
|
{
|
||
|
let task = RetrieveImageTask()
|
||
|
let options = currentDefaultOptions + (options ?? KingfisherEmptyOptionsInfo)
|
||
|
if options.forceRefresh {
|
||
|
_ = downloadAndCacheImage(
|
||
|
with: resource.downloadURL,
|
||
|
forKey: resource.cacheKey,
|
||
|
retrieveImageTask: task,
|
||
|
progressBlock: progressBlock,
|
||
|
completionHandler: completionHandler,
|
||
|
options: options)
|
||
|
} else {
|
||
|
tryToRetrieveImageFromCache(
|
||
|
forKey: resource.cacheKey,
|
||
|
with: resource.downloadURL,
|
||
|
retrieveImageTask: task,
|
||
|
progressBlock: progressBlock,
|
||
|
completionHandler: completionHandler,
|
||
|
options: options)
|
||
|
}
|
||
|
|
||
|
return task
|
||
|
}
|
||
|
|
||
|
@discardableResult
|
||
|
func downloadAndCacheImage(with url: URL,
|
||
|
forKey key: String,
|
||
|
retrieveImageTask: RetrieveImageTask,
|
||
|
progressBlock: DownloadProgressBlock?,
|
||
|
completionHandler: CompletionHandler?,
|
||
|
options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
|
||
|
{
|
||
|
let downloader = options.downloader ?? self.downloader
|
||
|
let processQueue = self.processQueue
|
||
|
return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options,
|
||
|
progressBlock: { receivedSize, totalSize in
|
||
|
progressBlock?(receivedSize, totalSize)
|
||
|
},
|
||
|
completionHandler: { image, error, imageURL, originalData in
|
||
|
|
||
|
let targetCache = options.targetCache ?? self.cache
|
||
|
if let error = error, error.code == KingfisherError.notModified.rawValue {
|
||
|
// Not modified. Try to find the image from cache.
|
||
|
// (The image should be in cache. It should be guaranteed by the framework users.)
|
||
|
targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> Void in
|
||
|
completionHandler?(cacheImage, nil, cacheType, url)
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if let image = image, let originalData = originalData {
|
||
|
targetCache.store(image,
|
||
|
original: originalData,
|
||
|
forKey: key,
|
||
|
processorIdentifier:options.processor.identifier,
|
||
|
cacheSerializer: options.cacheSerializer,
|
||
|
toDisk: !options.cacheMemoryOnly,
|
||
|
completionHandler: {
|
||
|
guard options.waitForCache else { return }
|
||
|
|
||
|
let cacheType = targetCache.imageCachedType(forKey: key, processorIdentifier: options.processor.identifier)
|
||
|
completionHandler?(image, nil, cacheType, url)
|
||
|
})
|
||
|
|
||
|
if options.cacheOriginalImage && options.processor != DefaultImageProcessor.default {
|
||
|
let originalCache = options.originalCache ?? targetCache
|
||
|
let defaultProcessor = DefaultImageProcessor.default
|
||
|
processQueue.async {
|
||
|
if let originalImage = defaultProcessor.process(item: .data(originalData), options: options) {
|
||
|
originalCache.store(originalImage,
|
||
|
original: originalData,
|
||
|
forKey: key,
|
||
|
processorIdentifier: defaultProcessor.identifier,
|
||
|
cacheSerializer: options.cacheSerializer,
|
||
|
toDisk: !options.cacheMemoryOnly,
|
||
|
completionHandler: nil)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if options.waitForCache == false || image == nil {
|
||
|
completionHandler?(image, error, .none, url)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func tryToRetrieveImageFromCache(forKey key: String,
|
||
|
with url: URL,
|
||
|
retrieveImageTask: RetrieveImageTask,
|
||
|
progressBlock: DownloadProgressBlock?,
|
||
|
completionHandler: CompletionHandler?,
|
||
|
options: KingfisherOptionsInfo)
|
||
|
{
|
||
|
|
||
|
let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> Void in
|
||
|
completionHandler?(image, error, cacheType, imageURL)
|
||
|
}
|
||
|
|
||
|
func handleNoCache() {
|
||
|
if options.onlyFromCache {
|
||
|
let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
|
||
|
diskTaskCompletionHandler(nil, error, .none, url)
|
||
|
return
|
||
|
}
|
||
|
self.downloadAndCacheImage(
|
||
|
with: url,
|
||
|
forKey: key,
|
||
|
retrieveImageTask: retrieveImageTask,
|
||
|
progressBlock: progressBlock,
|
||
|
completionHandler: diskTaskCompletionHandler,
|
||
|
options: options)
|
||
|
|
||
|
}
|
||
|
|
||
|
let targetCache = options.targetCache ?? self.cache
|
||
|
let processQueue = self.processQueue
|
||
|
// First, try to get the exactly image from cache
|
||
|
targetCache.retrieveImage(forKey: key, options: options) { image, cacheType in
|
||
|
// If found, we could finish now.
|
||
|
if image != nil {
|
||
|
diskTaskCompletionHandler(image, nil, cacheType, url)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// If not found, and we are using a default processor, download it!
|
||
|
let processor = options.processor
|
||
|
guard processor != DefaultImageProcessor.default else {
|
||
|
handleNoCache()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// If processor is not the default one, we have a chance to check whether
|
||
|
// the original image is already in cache.
|
||
|
let originalCache = options.originalCache ?? targetCache
|
||
|
let optionsWithoutProcessor = options.removeAllMatchesIgnoringAssociatedValue(.processor(processor))
|
||
|
originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { image, cacheType in
|
||
|
// If we found the original image, there is no need to download it again.
|
||
|
// We could just apply processor to it now.
|
||
|
guard let image = image else {
|
||
|
handleNoCache()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
processQueue.async {
|
||
|
guard let processedImage = processor.process(item: .image(image), options: options) else {
|
||
|
options.callbackDispatchQueue.safeAsync {
|
||
|
diskTaskCompletionHandler(nil, nil, .none, url)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
targetCache.store(processedImage,
|
||
|
original: nil,
|
||
|
forKey: key,
|
||
|
processorIdentifier:options.processor.identifier,
|
||
|
cacheSerializer: options.cacheSerializer,
|
||
|
toDisk: !options.cacheMemoryOnly,
|
||
|
completionHandler: {
|
||
|
guard options.waitForCache else { return }
|
||
|
|
||
|
let cacheType = targetCache.imageCachedType(forKey: key, processorIdentifier: options.processor.identifier)
|
||
|
options.callbackDispatchQueue.safeAsync {
|
||
|
diskTaskCompletionHandler(processedImage, nil, cacheType, url)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
if options.waitForCache == false {
|
||
|
options.callbackDispatchQueue.safeAsync {
|
||
|
diskTaskCompletionHandler(processedImage, nil, .none, url)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|