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.
431 lines
14 KiB
431 lines
14 KiB
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2015-2021 Alexander Grebenyuk (github.com/kean).
|
|
|
|
import Foundation
|
|
|
|
// MARK: - ImageRequest
|
|
|
|
/// Represents an image request.
|
|
public struct ImageRequest: CustomStringConvertible {
|
|
|
|
// MARK: Parameters of the Request
|
|
|
|
/// The `URLRequest` used for loading an image.
|
|
public var urlRequest: URLRequest {
|
|
get {
|
|
switch ref.resource {
|
|
case let .url(url):
|
|
var request = URLRequest(url: url) // create lazily
|
|
if cachePolicy == .reloadIgnoringCachedData {
|
|
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
}
|
|
return request
|
|
case let .urlRequest(urlRequest):
|
|
return urlRequest
|
|
}
|
|
}
|
|
set {
|
|
mutate {
|
|
$0.resource = Resource.urlRequest(newValue)
|
|
$0.urlString = newValue.url?.absoluteString
|
|
}
|
|
}
|
|
}
|
|
|
|
var urlString: String? {
|
|
ref.urlString
|
|
}
|
|
|
|
/// The execution priority of the request. The priority affects the order in which the image
|
|
/// requests are executed.
|
|
public enum Priority: Int, Comparable {
|
|
case veryLow = 0, low, normal, high, veryHigh
|
|
|
|
var taskPriority: TaskPriority {
|
|
switch self {
|
|
case .veryLow: return .veryLow
|
|
case .low: return .low
|
|
case .normal: return .normal
|
|
case .high: return .high
|
|
case .veryHigh: return .veryHigh
|
|
}
|
|
}
|
|
|
|
public static func < (lhs: Priority, rhs: Priority) -> Bool {
|
|
lhs.rawValue < rhs.rawValue
|
|
}
|
|
}
|
|
|
|
/// The relative priority of the operation. The priority affects the order in which the image
|
|
/// requests are executed.`.normal` by default.
|
|
public var priority: Priority {
|
|
get { ref.priority }
|
|
set { mutate { $0.priority = newValue } }
|
|
}
|
|
|
|
public enum CachePolicy {
|
|
case `default`
|
|
/// The image should be loaded only from the originating source.
|
|
///
|
|
/// If you initialize the request with `URLRequest`, make sure to provide
|
|
/// the correct policy in the request too.
|
|
case reloadIgnoringCachedData
|
|
}
|
|
|
|
public var cachePolicy: CachePolicy {
|
|
get { ref.cachePolicy }
|
|
set { mutate { $0.cachePolicy = newValue } }
|
|
}
|
|
|
|
/// The request options. See `ImageRequestOptions` for more info.
|
|
public var options: ImageRequestOptions {
|
|
get { ref.options }
|
|
set { mutate { $0.options = newValue } }
|
|
}
|
|
|
|
/// Processor to be applied to the image. `nil` by default.
|
|
public var processors: [ImageProcessing] {
|
|
get { ref.processors }
|
|
set { mutate { $0.processors = newValue } }
|
|
}
|
|
|
|
// MARK: Initializers
|
|
|
|
/// Initializes a request with the given URL.
|
|
///
|
|
/// - parameter priority: The priority of the request, `.normal` by default.
|
|
/// - parameter options: Advanced image loading options.
|
|
/// - parameter processors: Image processors to be applied after the image is loaded.
|
|
///
|
|
/// `ImageRequest` allows you to set image processors, change the request priority and more:
|
|
///
|
|
/// ```swift
|
|
/// let request = ImageRequest(
|
|
/// url: URL(string: "http://...")!,
|
|
/// processors: [ImageProcessors.Resize(size: imageView.bounds.size)],
|
|
/// priority: .high
|
|
/// )
|
|
/// ```
|
|
public init(url: URL,
|
|
processors: [ImageProcessing] = [],
|
|
cachePolicy: CachePolicy = .default,
|
|
priority: Priority = .normal,
|
|
options: ImageRequestOptions = .init()) {
|
|
self.ref = Container(resource: Resource.url(url), processors: processors, cachePolicy: cachePolicy, priority: priority, options: options)
|
|
self.ref.urlString = url.absoluteString
|
|
// creating `.absoluteString` takes 50% of time of Request creation,
|
|
// it's still faster than using URLs as cache keys
|
|
}
|
|
|
|
/// Initializes a request with the given request.
|
|
///
|
|
/// - parameter priority: The priority of the request, `.normal` by default.
|
|
/// - parameter options: Advanced image loading options.
|
|
/// - parameter processors: Image processors to be applied after the image is loaded.
|
|
///
|
|
/// `ImageRequest` allows you to set image processors, change the request priority and more:
|
|
///
|
|
/// ```swift
|
|
/// let request = ImageRequest(
|
|
/// url: URLRequest(url: URL(string: "http://...")!),
|
|
/// processors: [ImageProcessors.Resize(size: imageView.bounds.size)],
|
|
/// priority: .high
|
|
/// )
|
|
/// ```
|
|
public init(urlRequest: URLRequest,
|
|
processors: [ImageProcessing] = [],
|
|
cachePolicy: CachePolicy = .default,
|
|
priority: ImageRequest.Priority = .normal,
|
|
options: ImageRequestOptions = .init()) {
|
|
self.ref = Container(resource: Resource.urlRequest(urlRequest), processors: processors, cachePolicy: cachePolicy, priority: priority, options: options)
|
|
self.ref.urlString = urlRequest.url?.absoluteString
|
|
}
|
|
|
|
// CoW:
|
|
|
|
private var ref: Container
|
|
|
|
private mutating func mutate(_ closure: (Container) -> Void) {
|
|
if !isKnownUniquelyReferenced(&ref) {
|
|
ref = Container(container: ref)
|
|
}
|
|
closure(ref)
|
|
}
|
|
|
|
/// Just like many Swift built-in types, `ImageRequest` uses CoW approach to
|
|
/// avoid memberwise retain/releases when `ImageRequest` is passed around.
|
|
private class Container {
|
|
var resource: Resource
|
|
var urlString: String? // memoized absoluteString
|
|
var cachePolicy: CachePolicy
|
|
var priority: ImageRequest.Priority
|
|
var options: ImageRequestOptions
|
|
var processors: [ImageProcessing]
|
|
|
|
deinit {
|
|
#if TRACK_ALLOCATIONS
|
|
Allocations.decrement("ImageRequest.Container")
|
|
#endif
|
|
}
|
|
|
|
/// Creates a resource with a default processor.
|
|
init(resource: Resource, processors: [ImageProcessing], cachePolicy: CachePolicy, priority: Priority, options: ImageRequestOptions) {
|
|
self.resource = resource
|
|
self.processors = processors
|
|
self.cachePolicy = cachePolicy
|
|
self.priority = priority
|
|
self.options = options
|
|
|
|
#if TRACK_ALLOCATIONS
|
|
Allocations.increment("ImageRequest.Container")
|
|
#endif
|
|
}
|
|
|
|
/// Creates a copy.
|
|
init(container ref: Container) {
|
|
self.resource = ref.resource
|
|
self.urlString = ref.urlString
|
|
self.processors = ref.processors
|
|
self.cachePolicy = ref.cachePolicy
|
|
self.priority = ref.priority
|
|
self.options = ref.options
|
|
|
|
#if TRACK_ALLOCATIONS
|
|
Allocations.increment("ImageRequest.Container")
|
|
#endif
|
|
}
|
|
|
|
var preferredURLString: String {
|
|
options.filteredURL ?? urlString ?? ""
|
|
}
|
|
}
|
|
|
|
/// Resource representation (either URL or URLRequest).
|
|
private enum Resource: CustomStringConvertible {
|
|
case url(URL)
|
|
case urlRequest(URLRequest)
|
|
|
|
var description: String {
|
|
switch self {
|
|
case let .url(url):
|
|
return "\(url)"
|
|
case let .urlRequest(urlRequest):
|
|
return "\(urlRequest)"
|
|
}
|
|
}
|
|
}
|
|
|
|
public var description: String {
|
|
return """
|
|
ImageRequest {
|
|
resource: \(ref.resource)
|
|
priority: \(ref.priority)
|
|
processors: \(ref.processors)
|
|
options: {
|
|
memoryCacheOptions: \(ref.options.memoryCacheOptions)
|
|
filteredURL: \(String(describing: ref.options.filteredURL))
|
|
cacheKey: \(String(describing: ref.options.cacheKey))
|
|
loadKey: \(String(describing: ref.options.loadKey))
|
|
userInfo: \(String(describing: ref.options.userInfo))
|
|
}
|
|
}
|
|
"""
|
|
}
|
|
}
|
|
|
|
// MARK: - ImageRequestOptions (Advanced Options)
|
|
|
|
public struct ImageRequestOptions {
|
|
/// The policy to use when reading or writing images to the memory cache.
|
|
///
|
|
/// Soft-deprecated in Nuke 9.2.
|
|
public struct MemoryCacheOptions {
|
|
/// `true` by default.
|
|
public var isReadAllowed = true
|
|
|
|
/// `true` by default.
|
|
public var isWriteAllowed = true
|
|
|
|
public init(isReadAllowed: Bool = true, isWriteAllowed: Bool = true) {
|
|
self.isReadAllowed = isReadAllowed
|
|
self.isWriteAllowed = isWriteAllowed
|
|
}
|
|
}
|
|
|
|
/// `MemoryCacheOptions()` (read allowed, write allowed) by default.
|
|
public var memoryCacheOptions: MemoryCacheOptions
|
|
|
|
/// Provide a `filteredURL` to be used as a key for caching in case the original URL
|
|
/// contains transient query parameters.
|
|
///
|
|
/// ```
|
|
/// let request = ImageRequest(
|
|
/// url: URL(string: "http://example.com/image.jpeg?token=123")!,
|
|
/// options: ImageRequestOptions(
|
|
/// filteredURL: "http://example.com/image.jpeg"
|
|
/// )
|
|
/// )
|
|
/// ```
|
|
public var filteredURL: String?
|
|
|
|
/// The **memory** cache key for final processed images. Set if you are not
|
|
/// happy with the default behavior.
|
|
///
|
|
/// By default, two requests are considered equivalent if they have the same
|
|
/// URLs and the same processors.
|
|
public var cacheKey: AnyHashable?
|
|
|
|
/// Returns a key that compares requests with regards to loading images.
|
|
///
|
|
/// The default key considers two requests equivalent if they have the same
|
|
/// `URLRequests` and the same processors. `URLRequests` are compared by
|
|
/// their `URL`, `cachePolicy`, and `allowsCellularAccess` properties.
|
|
public var loadKey: AnyHashable?
|
|
|
|
/// Custom info passed alongside the request.
|
|
public var userInfo: [AnyHashable: Any]
|
|
|
|
public init(memoryCacheOptions: MemoryCacheOptions = .init(),
|
|
filteredURL: String? = nil,
|
|
cacheKey: AnyHashable? = nil,
|
|
loadKey: AnyHashable? = nil,
|
|
userInfo: [AnyHashable: Any] = [:]) {
|
|
self.memoryCacheOptions = memoryCacheOptions
|
|
self.filteredURL = filteredURL
|
|
self.cacheKey = cacheKey
|
|
self.loadKey = loadKey
|
|
self.userInfo = userInfo
|
|
}
|
|
}
|
|
|
|
// MARK: - ImageRequestKeys (Internal)
|
|
|
|
extension ImageRequest {
|
|
|
|
// MARK: - Cache Keys
|
|
|
|
/// A key for processed image in memory cache.
|
|
func makeCacheKeyForFinalImage() -> ImageRequest.CacheKey {
|
|
CacheKey(request: self)
|
|
}
|
|
|
|
/// A key for processed image data in disk cache.
|
|
func makeCacheKeyForFinalImageData() -> String {
|
|
"\(ref.preferredURLString)\(ImageProcessors.Composition(processors).identifier)"
|
|
}
|
|
|
|
/// A key for original image data in disk cache.
|
|
func makeCacheKeyForOriginalImageData() -> String {
|
|
ref.preferredURLString
|
|
}
|
|
|
|
// MARK: - Load Keys
|
|
|
|
/// A key for deduplicating operations for fetching the processed image.
|
|
func makeLoadKeyForFinalImage() -> LoadKeyForProcessedImage {
|
|
LoadKeyForProcessedImage(
|
|
cacheKey: makeCacheKeyForFinalImage(),
|
|
loadKey: makeLoadKeyForOriginalImage()
|
|
)
|
|
}
|
|
|
|
/// A key for deduplicating operations for fetching the original image.
|
|
func makeLoadKeyForOriginalImage() -> LoadKeyForOriginalImage {
|
|
LoadKeyForOriginalImage(request: self)
|
|
}
|
|
|
|
// MARK: - Internals (Keys)
|
|
|
|
// Uniquely identifies a cache processed image.
|
|
struct CacheKey: Hashable {
|
|
let request: ImageRequest
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
if let customKey = request.ref.options.cacheKey {
|
|
hasher.combine(customKey)
|
|
} else {
|
|
hasher.combine(request.ref.preferredURLString)
|
|
}
|
|
}
|
|
|
|
static func == (lhs: CacheKey, rhs: CacheKey) -> Bool {
|
|
let lhs = lhs.request.ref, rhs = rhs.request.ref
|
|
if lhs.options.cacheKey != nil || rhs.options.cacheKey != nil {
|
|
return lhs.options.cacheKey == rhs.options.cacheKey
|
|
}
|
|
return lhs.preferredURLString == rhs.preferredURLString && lhs.processors == rhs.processors
|
|
}
|
|
}
|
|
|
|
// Uniquely identifies a task of retrieving the processed image.
|
|
struct LoadKeyForProcessedImage: Hashable {
|
|
let cacheKey: CacheKey
|
|
let loadKey: AnyHashable
|
|
}
|
|
|
|
// Uniquely identifies a task of retrieving the original image.
|
|
struct LoadKeyForOriginalImage: Hashable {
|
|
let request: ImageRequest
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
if let customKey = request.ref.options.loadKey {
|
|
hasher.combine(customKey)
|
|
} else {
|
|
hasher.combine(request.ref.preferredURLString)
|
|
}
|
|
}
|
|
|
|
static func == (lhs: LoadKeyForOriginalImage, rhs: LoadKeyForOriginalImage) -> Bool {
|
|
let (lhs, rhs) = (lhs.request, rhs.request)
|
|
if lhs.options.loadKey != nil || rhs.options.loadKey != nil {
|
|
return lhs.options.loadKey == rhs.options.loadKey
|
|
}
|
|
return Parameters(lhs) == Parameters(rhs)
|
|
}
|
|
|
|
private struct Parameters: Hashable {
|
|
let urlString: String?
|
|
let requestCachePolicy: CachePolicy
|
|
let cachePolicy: URLRequest.CachePolicy
|
|
let allowsCellularAccess: Bool
|
|
|
|
init(_ request: ImageRequest) {
|
|
self.urlString = request.ref.urlString
|
|
self.requestCachePolicy = request.cachePolicy
|
|
switch request.ref.resource {
|
|
case .url:
|
|
self.cachePolicy = .useProtocolCachePolicy
|
|
self.allowsCellularAccess = true
|
|
case let .urlRequest(urlRequest):
|
|
self.cachePolicy = urlRequest.cachePolicy
|
|
self.allowsCellularAccess = urlRequest.allowsCellularAccess
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ImageRequestConvertible
|
|
|
|
public protocol ImageRequestConvertible {
|
|
func asImageRequest() -> ImageRequest
|
|
}
|
|
|
|
extension ImageRequest: ImageRequestConvertible {
|
|
public func asImageRequest() -> ImageRequest {
|
|
self
|
|
}
|
|
}
|
|
|
|
extension URL: ImageRequestConvertible {
|
|
public func asImageRequest() -> ImageRequest {
|
|
ImageRequest(url: self)
|
|
}
|
|
}
|
|
|
|
extension URLRequest: ImageRequestConvertible {
|
|
public func asImageRequest() -> ImageRequest {
|
|
ImageRequest(urlRequest: self)
|
|
}
|
|
}
|