mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
336 lines
12 KiB
Swift
336 lines
12 KiB
Swift
|
|
// The MIT License (MIT)
|
||
|
|
//
|
||
|
|
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||
|
|
|
||
|
|
import Foundation
|
||
|
|
|
||
|
|
#if !os(macOS)
|
||
|
|
import UIKit
|
||
|
|
#endif
|
||
|
|
|
||
|
|
/// Represents an image request.
|
||
|
|
public struct ImageRequest {
|
||
|
|
|
||
|
|
// MARK: Parameters of the Request
|
||
|
|
|
||
|
|
internal var urlString: String? {
|
||
|
|
return _ref._urlString
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The `URLRequest` used for loading an image.
|
||
|
|
public var urlRequest: URLRequest {
|
||
|
|
get { return _ref.resource.urlRequest }
|
||
|
|
set {
|
||
|
|
_mutate {
|
||
|
|
$0.resource = Resource.urlRequest(newValue)
|
||
|
|
$0._urlString = newValue.url?.absoluteString
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Processor to be applied to the image. `Decompressor` by default.
|
||
|
|
///
|
||
|
|
/// Decompressing compressed image formats (such as JPEG) can significantly
|
||
|
|
/// improve drawing performance as it allows a bitmap representation to be
|
||
|
|
/// created in a background rather than on the main thread.
|
||
|
|
public var processor: AnyImageProcessor? {
|
||
|
|
get {
|
||
|
|
// Default processor on macOS is nil, on other platforms is Decompressor
|
||
|
|
#if !os(macOS)
|
||
|
|
return _ref._isDefaultProcessorUsed ? ImageRequest.decompressor : _ref._processor
|
||
|
|
#else
|
||
|
|
return _ref._isDefaultProcessorUsed ? nil : _ref._processor
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
set {
|
||
|
|
_mutate {
|
||
|
|
$0._isDefaultProcessorUsed = false
|
||
|
|
$0._processor = newValue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The policy to use when reading or writing images to the memory cache.
|
||
|
|
public struct MemoryCacheOptions {
|
||
|
|
/// `true` by default.
|
||
|
|
public var isReadAllowed = true
|
||
|
|
|
||
|
|
/// `true` by default.
|
||
|
|
public var isWriteAllowed = true
|
||
|
|
|
||
|
|
public init() {}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `MemoryCacheOptions()` (read allowed, write allowed) by default.
|
||
|
|
public var memoryCacheOptions: MemoryCacheOptions {
|
||
|
|
get { return _ref.memoryCacheOptions }
|
||
|
|
set { _mutate { $0.memoryCacheOptions = newValue } }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The execution priority of the request.
|
||
|
|
public enum Priority: Int, Comparable {
|
||
|
|
case veryLow = 0, low, normal, high, veryHigh
|
||
|
|
|
||
|
|
internal var queuePriority: Operation.QueuePriority {
|
||
|
|
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 {
|
||
|
|
return lhs.rawValue < rhs.rawValue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The relative priority of the operation. This value is used to influence
|
||
|
|
/// the order in which requests are executed. `.normal` by default.
|
||
|
|
public var priority: Priority {
|
||
|
|
get { return _ref.priority }
|
||
|
|
set { _mutate { $0.priority = newValue }}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a key that compares requests with regards to caching images.
|
||
|
|
///
|
||
|
|
/// The default key considers two requests equivalent it they have the same
|
||
|
|
/// `URLRequests` and the same processors. `URLRequests` are compared
|
||
|
|
/// just by their `URLs`.
|
||
|
|
public var cacheKey: AnyHashable? {
|
||
|
|
get { return _ref.cacheKey }
|
||
|
|
set { _mutate { $0.cacheKey = newValue } }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns a key that compares requests with regards to loading images.
|
||
|
|
///
|
||
|
|
/// The default key considers two requests equivalent it they have the same
|
||
|
|
/// `URLRequests` and the same processors. `URLRequests` are compared by
|
||
|
|
/// their `URL`, `cachePolicy`, and `allowsCellularAccess` properties.
|
||
|
|
public var loadKey: AnyHashable? {
|
||
|
|
get { return _ref.loadKey }
|
||
|
|
set { _mutate { $0.loadKey = newValue } }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// If decoding is disabled, when the image data is loaded, the pipeline is
|
||
|
|
/// not going to create an image from it and will produce the `.decodingFailed`
|
||
|
|
/// error instead. `false` by default.
|
||
|
|
var isDecodingDisabled: Bool {
|
||
|
|
// This only used by `ImagePreheater` right now
|
||
|
|
get { return _ref.isDecodingDisabled }
|
||
|
|
set { _mutate { $0.isDecodingDisabled = newValue } }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Custom info passed alongside the request.
|
||
|
|
public var userInfo: Any? {
|
||
|
|
get { return _ref.userInfo }
|
||
|
|
set { _mutate { $0.userInfo = newValue }}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: Initializers
|
||
|
|
|
||
|
|
/// Initializes a request with the given URL.
|
||
|
|
public init(url: URL) {
|
||
|
|
_ref = Container(resource: Resource.url(url))
|
||
|
|
_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.
|
||
|
|
public init(urlRequest: URLRequest) {
|
||
|
|
_ref = Container(resource: Resource.urlRequest(urlRequest))
|
||
|
|
_ref._urlString = urlRequest.url?.absoluteString
|
||
|
|
}
|
||
|
|
|
||
|
|
#if !os(macOS)
|
||
|
|
|
||
|
|
/// Initializes a request with the given URL.
|
||
|
|
/// - parameter processor: Custom image processer.
|
||
|
|
public init<Processor: ImageProcessing>(url: URL, processor: Processor) {
|
||
|
|
self.init(url: url)
|
||
|
|
self.processor = AnyImageProcessor(processor)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Initializes a request with the given request.
|
||
|
|
/// - parameter processor: Custom image processer.
|
||
|
|
public init<Processor: ImageProcessing>(urlRequest: URLRequest, processor: Processor) {
|
||
|
|
self.init(urlRequest: urlRequest)
|
||
|
|
self.processor = AnyImageProcessor(processor)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Initializes a request with the given URL.
|
||
|
|
/// - parameter targetSize: Size in pixels.
|
||
|
|
/// - parameter contentMode: An option for how to resize the image
|
||
|
|
/// to the target size.
|
||
|
|
public init(url: URL, targetSize: CGSize, contentMode: ImageDecompressor.ContentMode, upscale: Bool = false) {
|
||
|
|
self.init(url: url, processor: ImageDecompressor(
|
||
|
|
targetSize: targetSize,
|
||
|
|
contentMode: contentMode,
|
||
|
|
upscale: upscale
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Initializes a request with the given request.
|
||
|
|
/// - parameter targetSize: Size in pixels.
|
||
|
|
/// - parameter contentMode: An option for how to resize the image
|
||
|
|
/// to the target size.
|
||
|
|
public init(urlRequest: URLRequest, targetSize: CGSize, contentMode: ImageDecompressor.ContentMode, upscale: Bool = false) {
|
||
|
|
self.init(urlRequest: urlRequest, processor: ImageDecompressor(
|
||
|
|
targetSize: targetSize,
|
||
|
|
contentMode: contentMode,
|
||
|
|
upscale: upscale
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
fileprivate static let decompressor = AnyImageProcessor(ImageDecompressor())
|
||
|
|
|
||
|
|
#endif
|
||
|
|
|
||
|
|
// 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
|
||
|
|
// true unless user set a custom one, this allows us not to store the
|
||
|
|
// default processor anywhere in the `Container` & skip equality tests
|
||
|
|
// when the default processor is used
|
||
|
|
var _isDefaultProcessorUsed: Bool = true
|
||
|
|
var _processor: AnyImageProcessor?
|
||
|
|
var memoryCacheOptions = MemoryCacheOptions()
|
||
|
|
var priority: ImageRequest.Priority = .normal
|
||
|
|
var cacheKey: AnyHashable?
|
||
|
|
var loadKey: AnyHashable?
|
||
|
|
var isDecodingDisabled: Bool = false
|
||
|
|
var userInfo: Any?
|
||
|
|
|
||
|
|
/// Creates a resource with a default processor.
|
||
|
|
init(resource: Resource) {
|
||
|
|
self.resource = resource
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Creates a copy.
|
||
|
|
init(container ref: Container) {
|
||
|
|
self.resource = ref.resource
|
||
|
|
self._urlString = ref._urlString
|
||
|
|
self._isDefaultProcessorUsed = ref._isDefaultProcessorUsed
|
||
|
|
self._processor = ref._processor
|
||
|
|
self.memoryCacheOptions = ref.memoryCacheOptions
|
||
|
|
self.priority = ref.priority
|
||
|
|
self.cacheKey = ref.cacheKey
|
||
|
|
self.loadKey = ref.loadKey
|
||
|
|
self.isDecodingDisabled = ref.isDecodingDisabled
|
||
|
|
self.userInfo = ref.userInfo
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Resource representation (either URL or URLRequest).
|
||
|
|
private enum Resource {
|
||
|
|
case url(URL)
|
||
|
|
case urlRequest(URLRequest)
|
||
|
|
|
||
|
|
var urlRequest: URLRequest {
|
||
|
|
switch self {
|
||
|
|
case let .url(url): return URLRequest(url: url) // create lazily
|
||
|
|
case let .urlRequest(urlRequest): return urlRequest
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public extension ImageRequest {
|
||
|
|
/// Appends a processor to the request. You can append arbitrary number of
|
||
|
|
/// processors to the request.
|
||
|
|
mutating func process<P: ImageProcessing>(with processor: P) {
|
||
|
|
guard let existing = self.processor else {
|
||
|
|
self.processor = AnyImageProcessor(processor)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
// Chain new processor and the existing one.
|
||
|
|
self.processor = AnyImageProcessor(ImageProcessorComposition([existing, AnyImageProcessor(processor)]))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Appends a processor to the request. You can append arbitrary number of
|
||
|
|
/// processors to the request.
|
||
|
|
func processed<P: ImageProcessing>(with processor: P) -> ImageRequest {
|
||
|
|
var request = self
|
||
|
|
request.process(with: processor)
|
||
|
|
return request
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Appends a processor to the request. You can append arbitrary number of
|
||
|
|
/// processors to the request.
|
||
|
|
mutating func process<Key: Hashable>(key: Key, _ closure: @escaping (Image) -> Image?) {
|
||
|
|
process(with: AnonymousImageProcessor<Key>(key, closure))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Appends a processor to the request. You can append arbitrary number of
|
||
|
|
/// processors to the request.
|
||
|
|
func processed<Key: Hashable>(key: Key, _ closure: @escaping (Image) -> Image?) -> ImageRequest {
|
||
|
|
return processed(with: AnonymousImageProcessor<Key>(key, closure))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
internal extension ImageRequest {
|
||
|
|
struct CacheKey: Hashable {
|
||
|
|
let request: ImageRequest
|
||
|
|
|
||
|
|
func hash(into hasher: inout Hasher) {
|
||
|
|
if let customKey = request._ref.cacheKey {
|
||
|
|
hasher.combine(customKey)
|
||
|
|
} else {
|
||
|
|
hasher.combine(request._ref._urlString?.hashValue ?? 0)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static func == (lhs: CacheKey, rhs: CacheKey) -> Bool {
|
||
|
|
let lhs = lhs.request, rhs = rhs.request
|
||
|
|
if let lhsCustomKey = lhs._ref.cacheKey, let rhsCustomKey = rhs._ref.cacheKey {
|
||
|
|
return lhsCustomKey == rhsCustomKey
|
||
|
|
}
|
||
|
|
guard lhs._ref._urlString == rhs._ref._urlString else {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return (lhs._ref._isDefaultProcessorUsed && rhs._ref._isDefaultProcessorUsed)
|
||
|
|
|| (lhs.processor == rhs.processor)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct LoadKey: Hashable {
|
||
|
|
let request: ImageRequest
|
||
|
|
|
||
|
|
func hash(into hasher: inout Hasher) {
|
||
|
|
if let customKey = request._ref.loadKey {
|
||
|
|
hasher.combine(customKey)
|
||
|
|
} else {
|
||
|
|
hasher.combine(request._ref._urlString?.hashValue ?? 0)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static func == (lhs: LoadKey, rhs: LoadKey) -> Bool {
|
||
|
|
func isEqual(_ lhs: URLRequest, _ rhs: URLRequest) -> Bool {
|
||
|
|
return lhs.cachePolicy == rhs.cachePolicy
|
||
|
|
&& lhs.allowsCellularAccess == rhs.allowsCellularAccess
|
||
|
|
}
|
||
|
|
let lhs = lhs.request, rhs = rhs.request
|
||
|
|
if let lhsCustomKey = lhs._ref.loadKey, let rhsCustomKey = rhs._ref.loadKey {
|
||
|
|
return lhsCustomKey == rhsCustomKey
|
||
|
|
}
|
||
|
|
return lhs._ref._urlString == rhs._ref._urlString
|
||
|
|
&& isEqual(lhs.urlRequest, rhs.urlRequest)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|