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.
430 lines
20 KiB
430 lines
20 KiB
//
|
|
// KingfisherManager.swift
|
|
// Kingfisher
|
|
//
|
|
// Created by Wei Wang on 15/4/6.
|
|
//
|
|
// Copyright (c) 2019 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.
|
|
|
|
|
|
import Foundation
|
|
|
|
/// The downloading progress block type.
|
|
/// The parameter value is the `receivedSize` of current response.
|
|
/// The second parameter is the total expected data length from response's "Content-Length" header.
|
|
/// If the expected length is not available, this block will not be called.
|
|
public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void)
|
|
|
|
/// Represents the result of a Kingfisher retrieving image task.
|
|
public struct RetrieveImageResult {
|
|
|
|
/// Gets the image object of this result.
|
|
public let image: Image
|
|
|
|
/// Gets the cache source of the image. It indicates from which layer of cache this image is retrieved.
|
|
/// If the image is just downloaded from network, `.none` will be returned.
|
|
public let cacheType: CacheType
|
|
|
|
/// The `Source` from which the retrieve task begins.
|
|
public let source: Source
|
|
}
|
|
|
|
/// Main manager class of Kingfisher. It connects Kingfisher downloader and cache,
|
|
/// to provide a set of convenience methods to use Kingfisher for tasks.
|
|
/// You can use this class to retrieve an image via a specified URL from web or cache.
|
|
public class KingfisherManager {
|
|
|
|
/// Represents a shared manager used across Kingfisher.
|
|
/// Use this instance for getting or storing images with Kingfisher.
|
|
public static let shared = KingfisherManager()
|
|
|
|
// Mark: Public Properties
|
|
/// The `ImageCache` used by this manager. It is `ImageCache.default` by default.
|
|
/// If a cache is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be
|
|
/// used instead.
|
|
public var cache: ImageCache
|
|
|
|
/// The `ImageDownloader` used by this manager. It is `ImageDownloader.default` by default.
|
|
/// If a downloader is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be
|
|
/// used instead.
|
|
public var downloader: ImageDownloader
|
|
|
|
/// Default options used by the manager. This option will be used in
|
|
/// Kingfisher manager related methods, as well as all view extension methods.
|
|
/// You can also passing other options for each image task by sending an `options` parameter
|
|
/// to Kingfisher's APIs. The per image options will overwrite the default ones,
|
|
/// if the option exists in both.
|
|
public var defaultOptions = KingfisherOptionsInfo.empty
|
|
|
|
// Use `defaultOptions` to overwrite the `downloader` and `cache`.
|
|
private var currentDefaultOptions: KingfisherOptionsInfo {
|
|
return [.downloader(downloader), .targetCache(cache)] + defaultOptions
|
|
}
|
|
|
|
private let processingQueue: CallbackQueue
|
|
|
|
private convenience init() {
|
|
self.init(downloader: .default, cache: .default)
|
|
}
|
|
|
|
/// Creates an image setting manager with specified downloader and cache.
|
|
///
|
|
/// - Parameters:
|
|
/// - downloader: The image downloader used to download images.
|
|
/// - cache: The image cache which stores memory and disk images.
|
|
public init(downloader: ImageDownloader, cache: ImageCache) {
|
|
self.downloader = downloader
|
|
self.cache = cache
|
|
|
|
let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)"
|
|
processingQueue = .dispatch(DispatchQueue(label: processQueueName))
|
|
}
|
|
|
|
// Mark: Getting Images
|
|
|
|
/// Gets an image from a given resource.
|
|
///
|
|
/// - Parameters:
|
|
/// - resource: The `Resource` object defines data information like key or URL.
|
|
/// - options: Options to use when creating the animated image.
|
|
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
|
/// `expectedContentLength`, this block will not be called. `progressBlock` is always called in
|
|
/// main queue.
|
|
/// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked
|
|
/// from the `options.callbackQueue`. If not specified, the main queue will be used.
|
|
/// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource,
|
|
/// the started `DownloadTask` is returned. Otherwise, `nil` is returned.
|
|
///
|
|
/// - Note:
|
|
/// This method will first check whether the requested `resource` is already in cache or not. If cached,
|
|
/// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it
|
|
/// will download the `resource`, store it in cache, then call `completionHandler`.
|
|
///
|
|
@discardableResult
|
|
public func retrieveImage(
|
|
with resource: Resource,
|
|
options: KingfisherOptionsInfo? = nil,
|
|
progressBlock: DownloadProgressBlock? = nil,
|
|
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
|
|
{
|
|
let source = Source.network(resource)
|
|
return retrieveImage(
|
|
with: source, options: options, progressBlock: progressBlock, completionHandler: completionHandler
|
|
)
|
|
}
|
|
|
|
/// Gets an image from a given resource.
|
|
///
|
|
/// - Parameters:
|
|
/// - source: The `Source` object defines data information from network or a data provider.
|
|
/// - options: Options to use when creating the animated image.
|
|
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
|
/// `expectedContentLength`, this block will not be called. `progressBlock` is always called in
|
|
/// main queue.
|
|
/// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked
|
|
/// from the `options.callbackQueue`. If not specified, the main queue will be used.
|
|
/// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource,
|
|
/// the started `DownloadTask` is returned. Otherwise, `nil` is returned.
|
|
///
|
|
/// - Note:
|
|
/// This method will first check whether the requested `source` is already in cache or not. If cached,
|
|
/// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it
|
|
/// will try to load the `source`, store it in cache, then call `completionHandler`.
|
|
///
|
|
public func retrieveImage(
|
|
with source: Source,
|
|
options: KingfisherOptionsInfo? = nil,
|
|
progressBlock: DownloadProgressBlock? = nil,
|
|
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
|
|
{
|
|
let options = currentDefaultOptions + (options ?? .empty)
|
|
var info = KingfisherParsedOptionsInfo(options)
|
|
if let block = progressBlock {
|
|
info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
|
}
|
|
return retrieveImage(
|
|
with: source,
|
|
options: info,
|
|
completionHandler: completionHandler)
|
|
}
|
|
|
|
func retrieveImage(
|
|
with source: Source,
|
|
options: KingfisherParsedOptionsInfo,
|
|
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
|
|
{
|
|
if options.forceRefresh {
|
|
return loadAndCacheImage(
|
|
source: source,
|
|
options: options,
|
|
completionHandler: completionHandler)?.value
|
|
|
|
} else {
|
|
let loadedFromCache = retrieveImageFromCache(
|
|
source: source,
|
|
options: options,
|
|
completionHandler: completionHandler)
|
|
|
|
if loadedFromCache {
|
|
return nil
|
|
}
|
|
|
|
if options.onlyFromCache {
|
|
let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey))
|
|
completionHandler?(.failure(error))
|
|
return nil
|
|
}
|
|
|
|
return loadAndCacheImage(
|
|
source: source,
|
|
options: options,
|
|
completionHandler: completionHandler)?.value
|
|
}
|
|
}
|
|
|
|
func provideImage(
|
|
provider: ImageDataProvider,
|
|
options: KingfisherParsedOptionsInfo,
|
|
completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)?)
|
|
{
|
|
guard let completionHandler = completionHandler else { return }
|
|
provider.data { result in
|
|
switch result {
|
|
case .success(let data):
|
|
(options.processingQueue ?? self.processingQueue).execute {
|
|
let processor = options.processor
|
|
let processingItem = ImageProcessItem.data(data)
|
|
guard let image = processor.process(item: processingItem, options: options) else {
|
|
options.callbackQueue.execute {
|
|
let error = KingfisherError.processorError(
|
|
reason: .processingFailed(processor: processor, item: processingItem))
|
|
completionHandler(.failure(error))
|
|
}
|
|
return
|
|
}
|
|
|
|
options.callbackQueue.execute {
|
|
let result = ImageLoadingResult(image: image, url: nil, originalData: data)
|
|
completionHandler(.success(result))
|
|
}
|
|
}
|
|
case .failure(let error):
|
|
options.callbackQueue.execute {
|
|
let error = KingfisherError.imageSettingError(
|
|
reason: .dataProviderError(provider: provider, error: error))
|
|
completionHandler(.failure(error))
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
func loadAndCacheImage(
|
|
source: Source,
|
|
options: KingfisherParsedOptionsInfo,
|
|
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
|
|
{
|
|
func cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>)
|
|
{
|
|
switch result {
|
|
case .success(let value):
|
|
// Add image to cache.
|
|
let targetCache = options.targetCache ?? self.cache
|
|
targetCache.store(
|
|
value.image,
|
|
original: value.originalData,
|
|
forKey: source.cacheKey,
|
|
options: options,
|
|
toDisk: !options.cacheMemoryOnly)
|
|
{
|
|
_ in
|
|
if options.waitForCache {
|
|
let result = RetrieveImageResult(image: value.image, cacheType: .none, source: source)
|
|
completionHandler?(.success(result))
|
|
}
|
|
}
|
|
|
|
// Add original image to cache if necessary.
|
|
let needToCacheOriginalImage = options.cacheOriginalImage &&
|
|
options.processor != DefaultImageProcessor.default
|
|
if needToCacheOriginalImage {
|
|
let originalCache = options.originalCache ?? targetCache
|
|
originalCache.storeToDisk(
|
|
value.originalData,
|
|
forKey: source.cacheKey,
|
|
processorIdentifier: DefaultImageProcessor.default.identifier,
|
|
expiration: options.diskCacheExpiration)
|
|
}
|
|
|
|
if !options.waitForCache {
|
|
let result = RetrieveImageResult(image: value.image, cacheType: .none, source: source)
|
|
completionHandler?(.success(result))
|
|
}
|
|
|
|
case .failure(let error):
|
|
completionHandler?(.failure(error))
|
|
}
|
|
}
|
|
|
|
switch source {
|
|
case .network(let resource):
|
|
let downloader = options.downloader ?? self.downloader
|
|
guard let task = downloader.downloadImage(
|
|
with: resource.downloadURL,
|
|
options: options,
|
|
completionHandler: cacheImage) else {
|
|
return nil
|
|
}
|
|
return .download(task)
|
|
|
|
case .provider(let provider):
|
|
provideImage(provider: provider, options: options, completionHandler: cacheImage)
|
|
return .dataProviding
|
|
}
|
|
}
|
|
|
|
/// Retrieves image from memory or disk cache.
|
|
///
|
|
/// - Parameters:
|
|
/// - source: The target source from which to get image.
|
|
/// - key: The key to use when caching the image.
|
|
/// - url: Image request URL. This is not used when retrieving image from cache. It is just used for
|
|
/// `RetrieveImageResult` callback compatibility.
|
|
/// - options: Options on how to get the image from image cache.
|
|
/// - completionHandler: Called when the image retrieving finishes, either with succeeded
|
|
/// `RetrieveImageResult` or an error.
|
|
/// - Returns: `true` if the requested image or the original image before being processed is existing in cache.
|
|
/// Otherwise, this method returns `false`.
|
|
///
|
|
/// - Note:
|
|
/// The image retrieving could happen in either memory cache or disk cache. The `.processor` option in
|
|
/// `options` will be considered when searching in the cache. If no processed image is found, Kingfisher
|
|
/// will try to check whether an original version of that image is existing or not. If there is already an
|
|
/// original, Kingfisher retrieves it from cache and processes it. Then, the processed image will be store
|
|
/// back to cache for later use.
|
|
func retrieveImageFromCache(
|
|
source: Source,
|
|
options: KingfisherParsedOptionsInfo,
|
|
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> Bool
|
|
{
|
|
// 1. Check whether the image was already in target cache. If so, just get it.
|
|
let targetCache = options.targetCache ?? cache
|
|
let key = source.cacheKey
|
|
let targetImageCached = targetCache.imageCachedType(
|
|
forKey: key, processorIdentifier: options.processor.identifier)
|
|
|
|
let validCache = targetImageCached.cached &&
|
|
(options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory)
|
|
if validCache {
|
|
targetCache.retrieveImage(forKey: key, options: options) { result in
|
|
guard let completionHandler = completionHandler else { return }
|
|
options.callbackQueue.execute {
|
|
result.match(
|
|
onSuccess: { cacheResult in
|
|
let value: Result<RetrieveImageResult, KingfisherError>
|
|
if let image = cacheResult.image {
|
|
value = result.map {
|
|
RetrieveImageResult(image: image, cacheType: $0.cacheType, source: source)
|
|
}
|
|
} else {
|
|
value = .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))
|
|
}
|
|
completionHandler(value)
|
|
},
|
|
onFailure: { _ in
|
|
completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))))
|
|
}
|
|
)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// 2. Check whether the original image exists. If so, get it, process it, save to storage and return.
|
|
let originalCache = options.originalCache ?? targetCache
|
|
// No need to store the same file in the same cache again.
|
|
if originalCache === targetCache && options.processor == DefaultImageProcessor.default {
|
|
return false
|
|
}
|
|
|
|
// Check whether the unprocessed image existing or not.
|
|
let originalImageCached = originalCache.imageCachedType(
|
|
forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier).cached
|
|
if originalImageCached {
|
|
// Now we are ready to get found the original image from cache. We need the unprocessed image, so remove
|
|
// any processor from options first.
|
|
var optionsWithoutProcessor = options
|
|
optionsWithoutProcessor.processor = DefaultImageProcessor.default
|
|
originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in
|
|
|
|
result.match(
|
|
onSuccess: { cacheResult in
|
|
guard let image = cacheResult.image else {
|
|
return
|
|
}
|
|
|
|
let processor = options.processor
|
|
(options.processingQueue ?? self.processingQueue).execute {
|
|
let item = ImageProcessItem.image(image)
|
|
guard let processedImage = processor.process(item: item, options: options) else {
|
|
let error = KingfisherError.processorError(
|
|
reason: .processingFailed(processor: processor, item: item))
|
|
options.callbackQueue.execute { completionHandler?(.failure(error)) }
|
|
return
|
|
}
|
|
|
|
var cacheOptions = options
|
|
cacheOptions.callbackQueue = .untouch
|
|
targetCache.store(
|
|
processedImage,
|
|
forKey: key,
|
|
options: cacheOptions,
|
|
toDisk: !options.cacheMemoryOnly)
|
|
{
|
|
_ in
|
|
if options.waitForCache {
|
|
let value = RetrieveImageResult(image: processedImage, cacheType: .none, source: source)
|
|
options.callbackQueue.execute { completionHandler?(.success(value)) }
|
|
}
|
|
}
|
|
|
|
if !options.waitForCache {
|
|
let value = RetrieveImageResult(image: processedImage, cacheType: .none, source: source)
|
|
options.callbackQueue.execute { completionHandler?(.success(value)) }
|
|
}
|
|
}
|
|
},
|
|
onFailure: { _ in
|
|
// This should not happen actually, since we already confirmed `originalImageCached` is `true`.
|
|
// Just in case...
|
|
options.callbackQueue.execute {
|
|
completionHandler?(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|