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

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)
}
}
}
}
}
}
}