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

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