mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-11 07:43:28 +01:00
472 lines
17 KiB
Swift
472 lines
17 KiB
Swift
|
|
// The MIT License (MIT)
|
|||
|
|
//
|
|||
|
|
// Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).
|
|||
|
|
|
|||
|
|
import Foundation
|
|||
|
|
import Combine
|
|||
|
|
|
|||
|
|
#if os(iOS) || os(tvOS) || os(watchOS)
|
|||
|
|
import UIKit
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
#if os(watchOS)
|
|||
|
|
import WatchKit
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
#if os(macOS)
|
|||
|
|
import Cocoa
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
// MARK: - ImageRequest
|
|||
|
|
|
|||
|
|
/// Represents an image request.
|
|||
|
|
public struct ImageRequest: CustomStringConvertible {
|
|||
|
|
|
|||
|
|
// MARK: Parameters
|
|||
|
|
|
|||
|
|
/// Returns the request `URLRequest`.
|
|||
|
|
///
|
|||
|
|
/// Returns `nil` for publisher-based requests.
|
|||
|
|
public var urlRequest: URLRequest? {
|
|||
|
|
switch ref.resource {
|
|||
|
|
case .url(let url): return url.map { URLRequest(url: $0) } // create lazily
|
|||
|
|
case .urlRequest(let urlRequest): return urlRequest
|
|||
|
|
case .publisher: return nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns the request `URL`.
|
|||
|
|
///
|
|||
|
|
/// Returns `nil` for publisher-based requests.
|
|||
|
|
public var url: URL? {
|
|||
|
|
switch ref.resource {
|
|||
|
|
case .url(let url): return url
|
|||
|
|
case .urlRequest(let request): return request.url
|
|||
|
|
case .publisher: return nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns the ID of the underlying image. For URL-based request, it's an
|
|||
|
|
/// image URL. For publisher – a custom ID.
|
|||
|
|
public var imageId: String? {
|
|||
|
|
switch ref.resource {
|
|||
|
|
case .url(let url): return url?.absoluteString
|
|||
|
|
case .urlRequest(let urlRequest): return urlRequest.url?.absoluteString
|
|||
|
|
case .publisher(let publisher): return publisher.id
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// The relative priority of the request. The priority affects the order in
|
|||
|
|
/// which the requests are performed. `.normal` by default.
|
|||
|
|
public var priority: Priority {
|
|||
|
|
get { ref.priority }
|
|||
|
|
set { mutate { $0.priority = newValue } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Processor to be applied to the image. Empty by default.
|
|||
|
|
public var processors: [ImageProcessing] {
|
|||
|
|
get { ref.processors }
|
|||
|
|
set { mutate { $0.processors = newValue } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// The request options.
|
|||
|
|
public var options: Options {
|
|||
|
|
get { ref.options }
|
|||
|
|
set { mutate { $0.options = newValue } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Custom info passed alongside the request.
|
|||
|
|
public var userInfo: [UserInfoKey: Any] {
|
|||
|
|
get { ref.userInfo ?? [:] }
|
|||
|
|
set { mutate { $0.userInfo = newValue } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// The priority affecting the order in which the requests are performed.
|
|||
|
|
public enum Priority: Int, Comparable {
|
|||
|
|
case veryLow = 0, low, normal, high, veryHigh
|
|||
|
|
|
|||
|
|
public static func < (lhs: Priority, rhs: Priority) -> Bool {
|
|||
|
|
lhs.rawValue < rhs.rawValue
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A key use in `userInfo`.
|
|||
|
|
public struct UserInfoKey: Hashable, ExpressibleByStringLiteral {
|
|||
|
|
public let rawValue: String
|
|||
|
|
|
|||
|
|
public init(_ rawValue: String) {
|
|||
|
|
self.rawValue = rawValue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public init(stringLiteral value: String) {
|
|||
|
|
self.rawValue = value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// By default, a pipeline uses URLs as unique image identifiers for
|
|||
|
|
/// caching and task coalescing. You can override this behavior by
|
|||
|
|
/// providing an `imageIdKey` instead. For example, you can use it to remove
|
|||
|
|
/// transient query parameters from the request.
|
|||
|
|
///
|
|||
|
|
/// ```
|
|||
|
|
/// let request = ImageRequest(
|
|||
|
|
/// url: URL(string: "http://example.com/image.jpeg?token=123"),
|
|||
|
|
/// userInfo: [.imageIdKey: "http://example.com/image.jpeg"]
|
|||
|
|
/// )
|
|||
|
|
/// ```
|
|||
|
|
public static let imageIdKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/imageId"
|
|||
|
|
|
|||
|
|
/// The image scale to be used. By default, the scale matches the scale
|
|||
|
|
/// of the current display.
|
|||
|
|
public static let scaleKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/scale"
|
|||
|
|
|
|||
|
|
/// Specifies whether the pipeline should retreive or generate a thumbnail
|
|||
|
|
/// instead of a full image. The thumbnail creation is generally significantly
|
|||
|
|
/// more efficient, especially in terms of memory usage, than image resizing
|
|||
|
|
/// (`ImageProcessors.Resize`).
|
|||
|
|
///
|
|||
|
|
/// - note: You must be using the default image decoder to make it work.
|
|||
|
|
public static let thumbnailKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/thumbmnailKey"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: Initializers
|
|||
|
|
|
|||
|
|
/// Initializes a request with the given URL.
|
|||
|
|
///
|
|||
|
|
/// - parameter url: The request URL.
|
|||
|
|
/// - parameter processors: Processors to be apply to the image. `[]` by default.
|
|||
|
|
/// - parameter priority: The priority of the request, `.normal` by default.
|
|||
|
|
/// - parameter options: Image loading options. `[]` by default.
|
|||
|
|
/// - parameter userInfo: Custom info passed alongside the request. `nil` by default.
|
|||
|
|
///
|
|||
|
|
/// ```swift
|
|||
|
|
/// let request = ImageRequest(
|
|||
|
|
/// url: URL(string: "http://..."),
|
|||
|
|
/// processors: [ImageProcessors.Resize(size: imageView.bounds.size)],
|
|||
|
|
/// priority: .high
|
|||
|
|
/// )
|
|||
|
|
/// ```
|
|||
|
|
public init(url: URL?,
|
|||
|
|
processors: [ImageProcessing] = [],
|
|||
|
|
priority: Priority = .normal,
|
|||
|
|
options: Options = [],
|
|||
|
|
userInfo: [UserInfoKey: Any]? = nil) {
|
|||
|
|
self.ref = Container(
|
|||
|
|
resource: Resource.url(url),
|
|||
|
|
processors: processors,
|
|||
|
|
priority: priority,
|
|||
|
|
options: options,
|
|||
|
|
userInfo: userInfo
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Initializes a request with the given request.
|
|||
|
|
///
|
|||
|
|
/// - parameter urlRequest: The URLRequest describing the image request.
|
|||
|
|
/// - parameter processors: Processors to be apply to the image. `[]` by default.
|
|||
|
|
/// - parameter priority: The priority of the request, `.normal` by default.
|
|||
|
|
/// - parameter options: Image loading options. `[]` by default.
|
|||
|
|
/// - parameter userInfo: Custom info passed alongside the request. `nil` by default.
|
|||
|
|
///
|
|||
|
|
/// ```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] = [],
|
|||
|
|
priority: Priority = .normal,
|
|||
|
|
options: Options = [],
|
|||
|
|
userInfo: [UserInfoKey: Any]? = nil) {
|
|||
|
|
self.ref = Container(
|
|||
|
|
resource: Resource.urlRequest(urlRequest),
|
|||
|
|
processors: processors,
|
|||
|
|
priority: priority,
|
|||
|
|
options: options,
|
|||
|
|
userInfo: userInfo
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Initializes a request with the given data publisher.
|
|||
|
|
///
|
|||
|
|
/// - parameter id: Uniquely identifies the image data.
|
|||
|
|
/// - parameter data: A data publisher to be used for fetching image data.
|
|||
|
|
/// - parameter processors: Processors to be apply to the image. `[]` by default.
|
|||
|
|
/// - parameter priority: The priority of the request, `.normal` by default.
|
|||
|
|
/// - parameter options: Image loading options. `[]` by default.
|
|||
|
|
/// - parameter userInfo: Custom info passed alongside the request. `nil` by default.
|
|||
|
|
///
|
|||
|
|
/// For example, here is how you can use it with Photos framework (the
|
|||
|
|
/// `imageDataPublisher()` API is a convenience extension).
|
|||
|
|
///
|
|||
|
|
/// ```swift
|
|||
|
|
/// let request = ImageRequest(
|
|||
|
|
/// id: asset.localIdentifier,
|
|||
|
|
/// data: PHAssetManager.imageDataPublisher(for: asset)
|
|||
|
|
/// )
|
|||
|
|
/// ```
|
|||
|
|
///
|
|||
|
|
/// - warning: If you don't want data to be stored in the disk cache, make
|
|||
|
|
/// sure to create a pipeline without it or disable it on a per-request basis.
|
|||
|
|
/// You can also disable it dynamically using `ImagePipelineDelegate`.
|
|||
|
|
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
|||
|
|
public init<P>(id: String, data: P,
|
|||
|
|
processors: [ImageProcessing] = [],
|
|||
|
|
priority: Priority = .normal,
|
|||
|
|
options: Options = [],
|
|||
|
|
userInfo: [UserInfoKey: Any]? = nil) where P: Publisher, P.Output == Data {
|
|||
|
|
// It could technically be implemented without any special change to the
|
|||
|
|
// pipeline by using a custom DataLoader, disabling resumable data, and
|
|||
|
|
// passing a publisher in the request userInfo.
|
|||
|
|
self.ref = Container(
|
|||
|
|
resource: .publisher(DataPublisher(id: id, data)),
|
|||
|
|
processors: processors,
|
|||
|
|
priority: priority,
|
|||
|
|
options: options,
|
|||
|
|
userInfo: userInfo
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: Options
|
|||
|
|
|
|||
|
|
/// Image request options.
|
|||
|
|
public struct Options: OptionSet, Hashable {
|
|||
|
|
/// Returns a raw value.
|
|||
|
|
public let rawValue: UInt16
|
|||
|
|
|
|||
|
|
/// Initialializes options with a given raw values.
|
|||
|
|
public init(rawValue: UInt16) {
|
|||
|
|
self.rawValue = rawValue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Disables memory cache reads (`ImageCaching`).
|
|||
|
|
public static let disableMemoryCacheReads = Options(rawValue: 1 << 0)
|
|||
|
|
|
|||
|
|
/// Disables memory cache writes (`ImageCaching`).
|
|||
|
|
public static let disableMemoryCacheWrites = Options(rawValue: 1 << 1)
|
|||
|
|
|
|||
|
|
/// Disables both memory cache reads and writes (`ImageCaching`).
|
|||
|
|
public static let disableMemoryCache: Options = [.disableMemoryCacheReads, .disableMemoryCacheWrites]
|
|||
|
|
|
|||
|
|
/// Disables disk cache reads (`DataCaching`).
|
|||
|
|
public static let disableDiskCacheReads = Options(rawValue: 1 << 2)
|
|||
|
|
|
|||
|
|
/// Disables disk cache writes (`DataCaching`).
|
|||
|
|
public static let disableDiskCacheWrites = Options(rawValue: 1 << 3)
|
|||
|
|
|
|||
|
|
/// Disables both disk cache reads and writes (`DataCaching`).
|
|||
|
|
public static let disableDiskCache: Options = [.disableDiskCacheReads, .disableDiskCacheWrites]
|
|||
|
|
|
|||
|
|
/// The image should be loaded only from the originating source.
|
|||
|
|
///
|
|||
|
|
/// This option only works `ImageCaching` and `DataCaching`, but not
|
|||
|
|
/// `URLCache`. If you want to ignore `URLCache`, initialize the request
|
|||
|
|
/// with `URLRequest` with the respective policy
|
|||
|
|
public static let reloadIgnoringCachedData: Options = [.disableMemoryCacheReads, .disableDiskCacheReads]
|
|||
|
|
|
|||
|
|
/// Use existing cache data and fail if no cached data is available.
|
|||
|
|
public static let returnCacheDataDontLoad = Options(rawValue: 1 << 4)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Thumbnail options.
|
|||
|
|
///
|
|||
|
|
/// For more info, see https://developer.apple.com/documentation/imageio/cgimagesource/image_source_option_dictionary_keys
|
|||
|
|
public struct ThumbnailOptions: Hashable {
|
|||
|
|
/// The maximum width and height in pixels of a thumbnail. If this key
|
|||
|
|
/// is not specified, the width and height of a thumbnail is not limited
|
|||
|
|
/// and thumbnails may be as big as the image itself.
|
|||
|
|
public var maxPixelSize: CGFloat
|
|||
|
|
|
|||
|
|
/// Whether a thumbnail should be automatically created for an image if
|
|||
|
|
/// a thumbnail isn't present in the image source file. The thumbnail is
|
|||
|
|
/// created from the full image, subject to the limit specified by
|
|||
|
|
/// `maxPixelSize`.
|
|||
|
|
///
|
|||
|
|
/// By default, `true`.
|
|||
|
|
public var createThumbnailFromImageIfAbsent = true
|
|||
|
|
|
|||
|
|
/// Whether a thumbnail should be created from the full image even if a
|
|||
|
|
/// thumbnail is present in the image source file. The thumbnail is created
|
|||
|
|
/// from the full image, subject to the limit specified by
|
|||
|
|
/// `maxPixelSize`.
|
|||
|
|
///
|
|||
|
|
/// By default, `true`.
|
|||
|
|
public var createThumbnailFromImageAlways = true
|
|||
|
|
|
|||
|
|
/// Whether the thumbnail should be rotated and scaled according to the
|
|||
|
|
/// orientation and pixel aspect ratio of the full image.
|
|||
|
|
///
|
|||
|
|
/// By default, `true`.
|
|||
|
|
public var createThumbnailWithTransform = true
|
|||
|
|
|
|||
|
|
/// Specifies whether image decoding and caching should happen at image
|
|||
|
|
/// creation time.
|
|||
|
|
///
|
|||
|
|
/// By default, `true`.
|
|||
|
|
public var shouldCacheImmediately = true
|
|||
|
|
|
|||
|
|
public init(maxPixelSize: CGFloat,
|
|||
|
|
createThumbnailFromImageIfAbsent: Bool = true,
|
|||
|
|
createThumbnailFromImageAlways: Bool = true,
|
|||
|
|
createThumbnailWithTransform: Bool = true,
|
|||
|
|
shouldCacheImmediately: Bool = true) {
|
|||
|
|
self.maxPixelSize = maxPixelSize
|
|||
|
|
self.createThumbnailFromImageIfAbsent = createThumbnailFromImageIfAbsent
|
|||
|
|
self.createThumbnailFromImageAlways = createThumbnailFromImageAlways
|
|||
|
|
self.createThumbnailWithTransform = createThumbnailWithTransform
|
|||
|
|
self.shouldCacheImmediately = shouldCacheImmediately
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var identifier: String {
|
|||
|
|
"com.github/kean/nuke/thumbnail?mxs=\(maxPixelSize),options=\(createThumbnailFromImageIfAbsent)\(createThumbnailFromImageAlways)\(createThumbnailWithTransform)\(shouldCacheImmediately)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: Internal
|
|||
|
|
|
|||
|
|
private(set) var ref: Container
|
|||
|
|
|
|||
|
|
private mutating func mutate(_ closure: (Container) -> Void) {
|
|||
|
|
if !isKnownUniquelyReferenced(&ref) {
|
|||
|
|
ref = 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.
|
|||
|
|
final class Container {
|
|||
|
|
// It's benefitial to put resource before priority and options because
|
|||
|
|
// of the resource size/stride of 9/16. Priority (1 byte) and Options
|
|||
|
|
// (2 bytes) slot just right in the remaining space.
|
|||
|
|
let resource: Resource
|
|||
|
|
fileprivate(set) var priority: Priority
|
|||
|
|
fileprivate(set) var options: Options
|
|||
|
|
fileprivate(set) var processors: [ImageProcessing]
|
|||
|
|
fileprivate(set) var userInfo: [UserInfoKey: Any]?
|
|||
|
|
// After trimming down the request size, it is no longer
|
|||
|
|
// as beneficial using CoW for ImageRequest, but there
|
|||
|
|
// still is a small but measurable difference.
|
|||
|
|
|
|||
|
|
deinit {
|
|||
|
|
#if TRACK_ALLOCATIONS
|
|||
|
|
Allocations.decrement("ImageRequest.Container")
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Creates a resource with a default processor.
|
|||
|
|
init(resource: Resource, processors: [ImageProcessing], priority: Priority, options: Options, userInfo: [UserInfoKey: Any]?) {
|
|||
|
|
self.resource = resource
|
|||
|
|
self.processors = processors
|
|||
|
|
self.priority = priority
|
|||
|
|
self.options = options
|
|||
|
|
self.userInfo = userInfo
|
|||
|
|
|
|||
|
|
#if TRACK_ALLOCATIONS
|
|||
|
|
Allocations.increment("ImageRequest.Container")
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Creates a copy.
|
|||
|
|
init(_ ref: Container) {
|
|||
|
|
self.resource = ref.resource
|
|||
|
|
self.processors = ref.processors
|
|||
|
|
self.priority = ref.priority
|
|||
|
|
self.options = ref.options
|
|||
|
|
self.userInfo = ref.userInfo
|
|||
|
|
|
|||
|
|
#if TRACK_ALLOCATIONS
|
|||
|
|
Allocations.increment("ImageRequest.Container")
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Every case takes 8 bytes and the enum 9 bytes overall (use stride!)
|
|||
|
|
enum Resource: CustomStringConvertible {
|
|||
|
|
case url(URL?)
|
|||
|
|
case urlRequest(URLRequest)
|
|||
|
|
case publisher(DataPublisher)
|
|||
|
|
|
|||
|
|
var description: String {
|
|||
|
|
switch self {
|
|||
|
|
case .url(let url): return "\(url?.absoluteString ?? "nil")"
|
|||
|
|
case .urlRequest(let urlRequest): return "\(urlRequest)"
|
|||
|
|
case .publisher(let data): return "\(data)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public var description: String {
|
|||
|
|
"ImageRequest(resource: \(ref.resource), priority: \(priority), processors: \(processors), options: \(options), userInfo: \(userInfo))"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func withProcessors(_ processors: [ImageProcessing]) -> ImageRequest {
|
|||
|
|
var request = self
|
|||
|
|
request.processors = processors
|
|||
|
|
return request
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var preferredImageId: String {
|
|||
|
|
if let imageId = ref.userInfo?[.imageIdKey] as? String {
|
|||
|
|
return imageId
|
|||
|
|
}
|
|||
|
|
return imageId ?? ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var thubmnail: ThumbnailOptions? {
|
|||
|
|
ref.userInfo?[.thumbnailKey] as? ThumbnailOptions
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var scale: CGFloat? {
|
|||
|
|
guard let scale = ref.userInfo?[.scaleKey] as? NSNumber else {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return CGFloat(scale.floatValue)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var publisher: DataPublisher? {
|
|||
|
|
guard case .publisher(let publisher) = ref.resource else {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return publisher
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - ImageRequestConvertible
|
|||
|
|
|
|||
|
|
/// Represents a type that can be converted to an `ImageRequest`.
|
|||
|
|
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 Optional: ImageRequestConvertible where Wrapped == URL {
|
|||
|
|
public func asImageRequest() -> ImageRequest {
|
|||
|
|
ImageRequest(url: self)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension URLRequest: ImageRequestConvertible {
|
|||
|
|
public func asImageRequest() -> ImageRequest {
|
|||
|
|
ImageRequest(urlRequest: self)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension String: ImageRequestConvertible {
|
|||
|
|
public func asImageRequest() -> ImageRequest {
|
|||
|
|
ImageRequest(url: URL(string: self))
|
|||
|
|
}
|
|||
|
|
}
|