Project Setup
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// AFIError.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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 Alamofire
|
||||
import Foundation
|
||||
|
||||
/// `AFIError` is the error type returned by AlamofireImage.
|
||||
///
|
||||
/// - requestCancelled: The request was explicitly cancelled.
|
||||
/// - imageSerializationFailed: Response data could not be serialized into an image.
|
||||
public enum AFIError: Error {
|
||||
case requestCancelled
|
||||
case imageSerializationFailed
|
||||
case alamofireError(AFError)
|
||||
}
|
||||
|
||||
// MARK: - Error Booleans
|
||||
|
||||
extension AFIError {
|
||||
/// Returns `true` if the `AFIError` is a request cancellation error, `false` otherwise.
|
||||
public var isRequestCancelledError: Bool {
|
||||
if case .requestCancelled = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns `true` if the `AFIError` is an image serialization error, `false` otherwise.
|
||||
public var isImageSerializationFailedError: Bool {
|
||||
if case .imageSerializationFailed = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
public var isAlamofireError: Bool {
|
||||
if case .alamofireError = self { return true }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Descriptions
|
||||
|
||||
extension AFIError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .requestCancelled:
|
||||
return "The request was explicitly cancelled."
|
||||
case .imageSerializationFailed:
|
||||
return "Response data could not be serialized into an image."
|
||||
case let .alamofireError(error):
|
||||
return "Request failed due to an underlying Alamofire error: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Image.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
import UIKit
|
||||
public typealias Image = UIImage
|
||||
#elseif os(macOS)
|
||||
import Cocoa
|
||||
public typealias Image = NSImage
|
||||
#endif
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
//
|
||||
// ImageCache.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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 Alamofire
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
// MARK: ImageCache
|
||||
|
||||
/// The `ImageCache` protocol defines a set of APIs for adding, removing and fetching images from a cache.
|
||||
public protocol ImageCache {
|
||||
/// Adds the image to the cache with the given identifier.
|
||||
func add(_ image: Image, withIdentifier identifier: String)
|
||||
|
||||
/// Removes the image from the cache matching the given identifier.
|
||||
func removeImage(withIdentifier identifier: String) -> Bool
|
||||
|
||||
/// Removes all images stored in the cache.
|
||||
@discardableResult
|
||||
func removeAllImages() -> Bool
|
||||
|
||||
/// Returns the image in the cache associated with the given identifier.
|
||||
func image(withIdentifier identifier: String) -> Image?
|
||||
}
|
||||
|
||||
/// The `ImageRequestCache` protocol extends the `ImageCache` protocol by adding methods for adding, removing and
|
||||
/// fetching images from a cache given an `URLRequest` and additional identifier.
|
||||
public protocol ImageRequestCache: ImageCache {
|
||||
/// Adds the image to the cache using an identifier created from the request and identifier.
|
||||
func add(_ image: Image, for request: URLRequest, withIdentifier identifier: String?)
|
||||
|
||||
/// Removes the image from the cache using an identifier created from the request and identifier.
|
||||
func removeImage(for request: URLRequest, withIdentifier identifier: String?) -> Bool
|
||||
|
||||
/// Returns the image from the cache associated with an identifier created from the request and identifier.
|
||||
func image(for request: URLRequest, withIdentifier identifier: String?) -> Image?
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When
|
||||
/// the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously
|
||||
/// purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the
|
||||
/// internal access date of the image is updated.
|
||||
open class AutoPurgingImageCache: ImageRequestCache {
|
||||
class CachedImage {
|
||||
let image: Image
|
||||
let identifier: String
|
||||
let totalBytes: UInt64
|
||||
var lastAccessDate: Date
|
||||
|
||||
init(_ image: Image, identifier: String) {
|
||||
self.image = image
|
||||
self.identifier = identifier
|
||||
lastAccessDate = Date()
|
||||
|
||||
totalBytes = {
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
let size = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
|
||||
#elseif os(macOS)
|
||||
let size = CGSize(width: image.size.width, height: image.size.height)
|
||||
#endif
|
||||
|
||||
let bytesPerPixel: CGFloat = 4.0
|
||||
let bytesPerRow = size.width * bytesPerPixel
|
||||
let totalBytes = UInt64(bytesPerRow) * UInt64(size.height)
|
||||
|
||||
return totalBytes
|
||||
}()
|
||||
}
|
||||
|
||||
func accessImage() -> Image {
|
||||
lastAccessDate = Date()
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The current total memory usage in bytes of all images stored within the cache.
|
||||
open var memoryUsage: UInt64 {
|
||||
var memoryUsage: UInt64 = 0
|
||||
synchronizationQueue.sync(flags: [.barrier]) { memoryUsage = self.currentMemoryUsage }
|
||||
|
||||
return memoryUsage
|
||||
}
|
||||
|
||||
/// The total memory capacity of the cache in bytes.
|
||||
public let memoryCapacity: UInt64
|
||||
|
||||
/// The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory
|
||||
/// capacity drops below this limit.
|
||||
public let preferredMemoryUsageAfterPurge: UInt64
|
||||
|
||||
private let synchronizationQueue: DispatchQueue
|
||||
private var cachedImages: [String: CachedImage]
|
||||
private var currentMemoryUsage: UInt64
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initializes the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage
|
||||
/// after purge limit.
|
||||
///
|
||||
/// Please note, the memory capacity must always be greater than or equal to the preferred memory usage after purge.
|
||||
///
|
||||
/// - parameter memoryCapacity: The total memory capacity of the cache in bytes. `100 MB` by default.
|
||||
/// - parameter preferredMemoryUsageAfterPurge: The preferred memory usage after purge in bytes. `60 MB` by default.
|
||||
///
|
||||
/// - returns: The new `AutoPurgingImageCache` instance.
|
||||
public init(memoryCapacity: UInt64 = 100_000_000, preferredMemoryUsageAfterPurge: UInt64 = 60_000_000) {
|
||||
self.memoryCapacity = memoryCapacity
|
||||
self.preferredMemoryUsageAfterPurge = preferredMemoryUsageAfterPurge
|
||||
|
||||
precondition(memoryCapacity >= preferredMemoryUsageAfterPurge,
|
||||
"The `memoryCapacity` must be greater than or equal to `preferredMemoryUsageAfterPurge`")
|
||||
|
||||
cachedImages = [:]
|
||||
currentMemoryUsage = 0
|
||||
|
||||
synchronizationQueue = {
|
||||
let name = String(format: "org.alamofire.autopurgingimagecache-%08x%08x", arc4random(), arc4random())
|
||||
return DispatchQueue(label: name, attributes: .concurrent)
|
||||
}()
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
let notification = UIApplication.didReceiveMemoryWarningNotification
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(AutoPurgingImageCache.removeAllImages),
|
||||
name: notification,
|
||||
object: nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Add Image to Cache
|
||||
|
||||
/// Adds the image to the cache using an identifier created from the request and optional identifier.
|
||||
///
|
||||
/// - parameter image: The image to add to the cache.
|
||||
/// - parameter request: The request used to generate the image's unique identifier.
|
||||
/// - parameter identifier: The additional identifier to append to the image's unique identifier.
|
||||
open func add(_ image: Image, for request: URLRequest, withIdentifier identifier: String? = nil) {
|
||||
let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)
|
||||
add(image, withIdentifier: requestIdentifier)
|
||||
}
|
||||
|
||||
/// Adds the image to the cache with the given identifier.
|
||||
///
|
||||
/// - parameter image: The image to add to the cache.
|
||||
/// - parameter identifier: The identifier to use to uniquely identify the image.
|
||||
open func add(_ image: Image, withIdentifier identifier: String) {
|
||||
synchronizationQueue.async(flags: [.barrier]) {
|
||||
let cachedImage = CachedImage(image, identifier: identifier)
|
||||
|
||||
if let previousCachedImage = self.cachedImages[identifier] {
|
||||
self.currentMemoryUsage -= previousCachedImage.totalBytes
|
||||
}
|
||||
|
||||
self.cachedImages[identifier] = cachedImage
|
||||
self.currentMemoryUsage += cachedImage.totalBytes
|
||||
}
|
||||
|
||||
synchronizationQueue.async(flags: [.barrier]) {
|
||||
if self.currentMemoryUsage > self.memoryCapacity {
|
||||
let bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge
|
||||
|
||||
var sortedImages = self.cachedImages.map { $1 }
|
||||
|
||||
sortedImages.sort {
|
||||
let date1 = $0.lastAccessDate
|
||||
let date2 = $1.lastAccessDate
|
||||
|
||||
return date1.timeIntervalSince(date2) < 0.0
|
||||
}
|
||||
|
||||
var bytesPurged = UInt64(0)
|
||||
|
||||
for cachedImage in sortedImages {
|
||||
self.cachedImages.removeValue(forKey: cachedImage.identifier)
|
||||
bytesPurged += cachedImage.totalBytes
|
||||
|
||||
if bytesPurged >= bytesToPurge {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
self.currentMemoryUsage -= bytesPurged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Remove Image from Cache
|
||||
|
||||
/// Removes the image from the cache using an identifier created from the request and optional identifier.
|
||||
///
|
||||
/// - parameter request: The request used to generate the image's unique identifier.
|
||||
/// - parameter identifier: The additional identifier to append to the image's unique identifier.
|
||||
///
|
||||
/// - returns: `true` if the image was removed, `false` otherwise.
|
||||
@discardableResult
|
||||
open func removeImage(for request: URLRequest, withIdentifier identifier: String?) -> Bool {
|
||||
let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)
|
||||
return removeImage(withIdentifier: requestIdentifier)
|
||||
}
|
||||
|
||||
/// Removes all images from the cache created from the request.
|
||||
///
|
||||
/// - parameter request: The request used to generate the image's unique identifier.
|
||||
///
|
||||
/// - returns: `true` if any images were removed, `false` otherwise.
|
||||
@discardableResult
|
||||
open func removeImages(matching request: URLRequest) -> Bool {
|
||||
let requestIdentifier = imageCacheKey(for: request, withIdentifier: nil)
|
||||
var removed = false
|
||||
|
||||
synchronizationQueue.sync(flags: [.barrier]) {
|
||||
for key in self.cachedImages.keys where key.hasPrefix(requestIdentifier) {
|
||||
if let cachedImage = self.cachedImages.removeValue(forKey: key) {
|
||||
self.currentMemoryUsage -= cachedImage.totalBytes
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
/// Removes the image from the cache matching the given identifier.
|
||||
///
|
||||
/// - parameter identifier: The unique identifier for the image.
|
||||
///
|
||||
/// - returns: `true` if the image was removed, `false` otherwise.
|
||||
@discardableResult
|
||||
open func removeImage(withIdentifier identifier: String) -> Bool {
|
||||
var removed = false
|
||||
|
||||
synchronizationQueue.sync(flags: [.barrier]) {
|
||||
if let cachedImage = self.cachedImages.removeValue(forKey: identifier) {
|
||||
self.currentMemoryUsage -= cachedImage.totalBytes
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
/// Removes all images stored in the cache.
|
||||
///
|
||||
/// - returns: `true` if images were removed from the cache, `false` otherwise.
|
||||
@discardableResult @objc
|
||||
open func removeAllImages() -> Bool {
|
||||
var removed = false
|
||||
|
||||
synchronizationQueue.sync(flags: [.barrier]) {
|
||||
if !self.cachedImages.isEmpty {
|
||||
self.cachedImages.removeAll()
|
||||
self.currentMemoryUsage = 0
|
||||
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
// MARK: Fetch Image from Cache
|
||||
|
||||
/// Returns the image from the cache associated with an identifier created from the request and optional identifier.
|
||||
///
|
||||
/// - parameter request: The request used to generate the image's unique identifier.
|
||||
/// - parameter identifier: The additional identifier to append to the image's unique identifier.
|
||||
///
|
||||
/// - returns: The image if it is stored in the cache, `nil` otherwise.
|
||||
open func image(for request: URLRequest, withIdentifier identifier: String? = nil) -> Image? {
|
||||
let requestIdentifier = imageCacheKey(for: request, withIdentifier: identifier)
|
||||
return image(withIdentifier: requestIdentifier)
|
||||
}
|
||||
|
||||
/// Returns the image in the cache associated with the given identifier.
|
||||
///
|
||||
/// - parameter identifier: The unique identifier for the image.
|
||||
///
|
||||
/// - returns: The image if it is stored in the cache, `nil` otherwise.
|
||||
open func image(withIdentifier identifier: String) -> Image? {
|
||||
var image: Image?
|
||||
|
||||
synchronizationQueue.sync(flags: [.barrier]) {
|
||||
if let cachedImage = self.cachedImages[identifier] {
|
||||
image = cachedImage.accessImage()
|
||||
}
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
// MARK: Image Cache Keys
|
||||
|
||||
/// Returns the unique image cache key for the specified request and additional identifier.
|
||||
///
|
||||
/// - parameter request: The request.
|
||||
/// - parameter identifier: The additional identifier.
|
||||
///
|
||||
/// - returns: The unique image cache key.
|
||||
open func imageCacheKey(for request: URLRequest, withIdentifier identifier: String?) -> String {
|
||||
var key = request.url?.absoluteString ?? ""
|
||||
|
||||
if let identifier = identifier {
|
||||
key += "-\(identifier)"
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
//
|
||||
// ImageDownloader.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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 Alamofire
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
/// Alias for `DataResponse<T, AFIError>`.
|
||||
public typealias AFIDataResponse<T> = DataResponse<T, AFIError>
|
||||
|
||||
/// Alias for `Result<T, AFIError>`.
|
||||
public typealias AFIResult<T> = Result<T, AFIError>
|
||||
|
||||
/// The `RequestReceipt` is an object vended by the `ImageDownloader` when starting a download request. It can be used
|
||||
/// to cancel active requests running on the `ImageDownloader` session. As a general rule, image download requests
|
||||
/// should be cancelled using the `RequestReceipt` instead of calling `cancel` directly on the `request` itself. The
|
||||
/// `ImageDownloader` is optimized to handle duplicate request scenarios as well as pending versus active downloads.
|
||||
open class RequestReceipt {
|
||||
/// The download request created by the `ImageDownloader`.
|
||||
public let request: DataRequest
|
||||
|
||||
/// The unique identifier for the image filters and completion handlers when duplicate requests are made.
|
||||
public let receiptID: String
|
||||
|
||||
init(request: DataRequest, receiptID: String) {
|
||||
self.request = request
|
||||
self.receiptID = receiptID
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// The `ImageDownloader` class is responsible for downloading images in parallel on a prioritized queue. Incoming
|
||||
/// downloads are added to the front or back of the queue depending on the download prioritization. Each downloaded
|
||||
/// image is cached in the underlying `NSURLCache` as well as the in-memory image cache that supports image filters.
|
||||
/// By default, any download request with a cached image equivalent in the image cache will automatically be served the
|
||||
/// cached image representation. Additional advanced features include supporting multiple image filters and completion
|
||||
/// handlers for a single request.
|
||||
open class ImageDownloader {
|
||||
/// The completion handler closure used when an image download completes.
|
||||
public typealias CompletionHandler = (AFIDataResponse<Image>) -> Void
|
||||
|
||||
/// The progress handler closure called periodically during an image download.
|
||||
public typealias ProgressHandler = DataRequest.ProgressHandler
|
||||
|
||||
// MARK: Helper Types
|
||||
|
||||
/// Defines the order prioritization of incoming download requests being inserted into the queue.
|
||||
///
|
||||
/// - fifo: All incoming downloads are added to the back of the queue.
|
||||
/// - lifo: All incoming downloads are added to the front of the queue.
|
||||
public enum DownloadPrioritization {
|
||||
case fifo, lifo
|
||||
}
|
||||
|
||||
final class ResponseHandler {
|
||||
let urlID: String
|
||||
let handlerID: String
|
||||
let request: DataRequest
|
||||
var operations: [(receiptID: String, filter: ImageFilter?, completion: CompletionHandler?)]
|
||||
|
||||
init(request: DataRequest,
|
||||
handlerID: String,
|
||||
receiptID: String,
|
||||
filter: ImageFilter?,
|
||||
completion: CompletionHandler?) {
|
||||
self.request = request
|
||||
urlID = ImageDownloader.urlIdentifier(for: request.convertible)
|
||||
self.handlerID = handlerID
|
||||
operations = [(receiptID: receiptID, filter: filter, completion: completion)]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The image cache used to store all downloaded images in.
|
||||
public let imageCache: ImageRequestCache?
|
||||
|
||||
/// The credential used for authenticating each download request.
|
||||
open private(set) var credential: URLCredential?
|
||||
|
||||
/// Response serializer used to convert the image data to UIImage.
|
||||
public var imageResponseSerializer = ImageResponseSerializer()
|
||||
|
||||
/// The underlying Alamofire `Session` instance used to handle all download requests.
|
||||
public let session: Session
|
||||
|
||||
let downloadPrioritization: DownloadPrioritization
|
||||
let maximumActiveDownloads: Int
|
||||
|
||||
var activeRequestCount = 0
|
||||
var queuedRequests: [Request] = []
|
||||
var responseHandlers: [String: ResponseHandler] = [:]
|
||||
|
||||
private let synchronizationQueue: DispatchQueue = {
|
||||
let name = String(format: "org.alamofire.imagedownloader.synchronizationqueue-%08x%08x", arc4random(), arc4random())
|
||||
return DispatchQueue(label: name)
|
||||
}()
|
||||
|
||||
private let responseQueue: DispatchQueue = {
|
||||
let name = String(format: "org.alamofire.imagedownloader.responsequeue-%08x%08x", arc4random(), arc4random())
|
||||
return DispatchQueue(label: name, attributes: .concurrent)
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// The default instance of `ImageDownloader` initialized with default values.
|
||||
public static let `default` = ImageDownloader()
|
||||
|
||||
/// Creates a default `URLSessionConfiguration` with common usage parameter values.
|
||||
///
|
||||
/// - returns: The default `URLSessionConfiguration` instance.
|
||||
open class func defaultURLSessionConfiguration() -> URLSessionConfiguration {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
|
||||
configuration.headers = .default
|
||||
configuration.httpShouldSetCookies = true
|
||||
configuration.httpShouldUsePipelining = false
|
||||
|
||||
configuration.requestCachePolicy = .useProtocolCachePolicy
|
||||
configuration.allowsCellularAccess = true
|
||||
configuration.timeoutIntervalForRequest = 60
|
||||
|
||||
configuration.urlCache = ImageDownloader.defaultURLCache()
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
/// Creates a default `URLCache` with common usage parameter values.
|
||||
///
|
||||
/// - returns: The default `URLCache` instance.
|
||||
open class func defaultURLCache() -> URLCache {
|
||||
let memoryCapacity = 20 * 1024 * 1024
|
||||
let diskCapacity = 150 * 1024 * 1024
|
||||
let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||||
let imageDownloaderPath = "org.alamofire.imagedownloader"
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
return URLCache(memoryCapacity: memoryCapacity,
|
||||
diskCapacity: diskCapacity,
|
||||
directory: cacheDirectory?.appendingPathComponent(imageDownloaderPath))
|
||||
#else
|
||||
#if os(macOS)
|
||||
return URLCache(memoryCapacity: memoryCapacity,
|
||||
diskCapacity: diskCapacity,
|
||||
diskPath: cacheDirectory?.appendingPathComponent(imageDownloaderPath).path)
|
||||
#else
|
||||
return URLCache(memoryCapacity: memoryCapacity,
|
||||
diskCapacity: diskCapacity,
|
||||
diskPath: imageDownloaderPath)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Initializes the `ImageDownloader` instance with the given configuration, download prioritization, maximum active
|
||||
/// download count and image cache.
|
||||
///
|
||||
/// - parameter configuration: The `URLSessionConfiguration` to use to create the underlying Alamofire
|
||||
/// `SessionManager` instance.
|
||||
/// - parameter downloadPrioritization: The download prioritization of the download queue. `.fifo` by default.
|
||||
/// - parameter maximumActiveDownloads: The maximum number of active downloads allowed at any given time.
|
||||
/// - parameter imageCache: The image cache used to store all downloaded images in.
|
||||
///
|
||||
/// - returns: The new `ImageDownloader` instance.
|
||||
public init(configuration: URLSessionConfiguration = ImageDownloader.defaultURLSessionConfiguration(),
|
||||
downloadPrioritization: DownloadPrioritization = .fifo,
|
||||
maximumActiveDownloads: Int = 4,
|
||||
imageCache: ImageRequestCache? = AutoPurgingImageCache()) {
|
||||
session = Session(configuration: configuration, startRequestsImmediately: false)
|
||||
self.downloadPrioritization = downloadPrioritization
|
||||
self.maximumActiveDownloads = maximumActiveDownloads
|
||||
self.imageCache = imageCache
|
||||
}
|
||||
|
||||
/// Initializes the `ImageDownloader` instance with the given session manager, download prioritization, maximum
|
||||
/// active download count and image cache.
|
||||
///
|
||||
/// - parameter session: The Alamofire `Session` instance to handle all download requests.
|
||||
/// - parameter downloadPrioritization: The download prioritization of the download queue. `.fifo` by default.
|
||||
/// - parameter maximumActiveDownloads: The maximum number of active downloads allowed at any given time.
|
||||
/// - parameter imageCache: The image cache used to store all downloaded images in.
|
||||
///
|
||||
/// - returns: The new `ImageDownloader` instance.
|
||||
public init(session: Session,
|
||||
downloadPrioritization: DownloadPrioritization = .fifo,
|
||||
maximumActiveDownloads: Int = 4,
|
||||
imageCache: ImageRequestCache? = AutoPurgingImageCache()) {
|
||||
precondition(!session.startRequestsImmediately, "Session must set `startRequestsImmediately` to `false`.")
|
||||
|
||||
self.session = session
|
||||
self.downloadPrioritization = downloadPrioritization
|
||||
self.maximumActiveDownloads = maximumActiveDownloads
|
||||
self.imageCache = imageCache
|
||||
}
|
||||
|
||||
// MARK: Authentication
|
||||
|
||||
/// Associates an HTTP Basic Auth credential with all future download requests.
|
||||
///
|
||||
/// - parameter user: The user.
|
||||
/// - parameter password: The password.
|
||||
/// - parameter persistence: The URL credential persistence. `.forSession` by default.
|
||||
open func addAuthentication(user: String,
|
||||
password: String,
|
||||
persistence: URLCredential.Persistence = .forSession) {
|
||||
let credential = URLCredential(user: user, password: password, persistence: persistence)
|
||||
addAuthentication(usingCredential: credential)
|
||||
}
|
||||
|
||||
/// Associates the specified credential with all future download requests.
|
||||
///
|
||||
/// - parameter credential: The credential.
|
||||
open func addAuthentication(usingCredential credential: URLCredential) {
|
||||
synchronizationQueue.sync {
|
||||
self.credential = credential
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Download
|
||||
|
||||
/// Creates a download request using the internal Alamofire `SessionManager` instance for the specified URL request.
|
||||
///
|
||||
/// If the same download request is already in the queue or currently being downloaded, the filter and completion
|
||||
/// handler are appended to the already existing request. Once the request completes, all filters and completion
|
||||
/// handlers attached to the request are executed in the order they were added. Additionally, any filters attached
|
||||
/// to the request with the same identifiers are only executed once. The resulting image is then passed into each
|
||||
/// completion handler paired with the filter.
|
||||
///
|
||||
/// You should not attempt to directly cancel the `request` inside the request receipt since other callers may be
|
||||
/// relying on the completion of that request. Instead, you should call `cancelRequestForRequestReceipt` with the
|
||||
/// returned request receipt to allow the `ImageDownloader` to optimize the cancellation on behalf of all active
|
||||
/// callers.
|
||||
///
|
||||
/// - parameter urlRequest: The URL request.
|
||||
/// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults to `nil`.
|
||||
/// - parameter receiptID: The `identifier` for the `RequestReceipt` returned. Defaults to a new, randomly
|
||||
/// generated UUID.
|
||||
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`. Defaults
|
||||
/// to `nil` which will fall back to the instance `imageResponseSerializer`.
|
||||
/// - parameter filter: The image filter to apply to the image after the download is complete. Defaults
|
||||
/// to `nil`.
|
||||
/// - parameter progress: The closure to be executed periodically during the lifecycle of the request.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue.
|
||||
/// - parameter completion: The closure called when the download request is complete. Defaults to `nil`.
|
||||
///
|
||||
/// - returns: The request receipt for the download request if available. `nil` if the image is stored in the image
|
||||
/// cache and the URL request cache policy allows the cache to be used.
|
||||
@discardableResult
|
||||
open func download(_ urlRequest: URLRequestConvertible,
|
||||
cacheKey: String? = nil,
|
||||
receiptID: String = UUID().uuidString,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: CompletionHandler? = nil)
|
||||
-> RequestReceipt? {
|
||||
var queuedRequest: DataRequest?
|
||||
|
||||
synchronizationQueue.sync {
|
||||
// 1) Append the filter and completion handler to a pre-existing request if it already exists
|
||||
let urlID = ImageDownloader.urlIdentifier(for: urlRequest)
|
||||
|
||||
if let responseHandler = self.responseHandlers[urlID] {
|
||||
responseHandler.operations.append((receiptID: receiptID, filter: filter, completion: completion))
|
||||
queuedRequest = responseHandler.request
|
||||
return
|
||||
}
|
||||
|
||||
// 2) Attempt to load the image from the image cache if the cache policy allows it
|
||||
if let nonNilURLRequest = urlRequest.urlRequest {
|
||||
switch nonNilURLRequest.cachePolicy {
|
||||
case .useProtocolCachePolicy, .returnCacheDataElseLoad, .returnCacheDataDontLoad:
|
||||
let cachedImage: Image?
|
||||
|
||||
if let cacheKey = cacheKey {
|
||||
cachedImage = self.imageCache?.image(withIdentifier: cacheKey)
|
||||
} else {
|
||||
cachedImage = self.imageCache?.image(for: nonNilURLRequest, withIdentifier: filter?.identifier)
|
||||
}
|
||||
|
||||
if let image = cachedImage {
|
||||
DispatchQueue.main.async {
|
||||
let response = AFIDataResponse<Image>(request: urlRequest.urlRequest,
|
||||
response: nil,
|
||||
data: nil,
|
||||
metrics: nil,
|
||||
serializationDuration: 0.0,
|
||||
result: .success(image))
|
||||
|
||||
completion?(response)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Create the request and set up authentication, validation and response serialization
|
||||
let request = self.session.request(urlRequest)
|
||||
queuedRequest = request
|
||||
|
||||
if let credential = self.credential {
|
||||
request.authenticate(with: credential)
|
||||
}
|
||||
|
||||
request.validate()
|
||||
|
||||
if let progress = progress {
|
||||
request.downloadProgress(queue: progressQueue, closure: progress)
|
||||
}
|
||||
|
||||
// Generate a unique handler id to check whether the active request has changed while downloading
|
||||
let handlerID = UUID().uuidString
|
||||
|
||||
request.response(queue: self.responseQueue,
|
||||
responseSerializer: serializer ?? imageResponseSerializer,
|
||||
completionHandler: { response in
|
||||
defer {
|
||||
self.safelyDecrementActiveRequestCount()
|
||||
self.safelyStartNextRequestIfNecessary()
|
||||
}
|
||||
|
||||
// Early out if the request has changed out from under us
|
||||
guard
|
||||
let handler = self.safelyFetchResponseHandler(withURLIdentifier: urlID),
|
||||
handler.handlerID == handlerID,
|
||||
let responseHandler = self.safelyRemoveResponseHandler(withURLIdentifier: urlID)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
switch response.result {
|
||||
case let .success(image):
|
||||
var filteredImages: [String: Image] = [:]
|
||||
|
||||
for (_, filter, completion) in responseHandler.operations {
|
||||
var filteredImage: Image
|
||||
|
||||
if let filter = filter {
|
||||
if let alreadyFilteredImage = filteredImages[filter.identifier] {
|
||||
filteredImage = alreadyFilteredImage
|
||||
} else {
|
||||
filteredImage = filter.filter(image)
|
||||
filteredImages[filter.identifier] = filteredImage
|
||||
}
|
||||
} else {
|
||||
filteredImage = image
|
||||
}
|
||||
|
||||
if let cacheKey = cacheKey {
|
||||
self.imageCache?.add(filteredImage, withIdentifier: cacheKey)
|
||||
} else if let request = response.request {
|
||||
self.imageCache?.add(filteredImage, for: request, withIdentifier: filter?.identifier)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let response = AFIDataResponse<Image>(request: response.request,
|
||||
response: response.response,
|
||||
data: response.data,
|
||||
metrics: response.metrics,
|
||||
serializationDuration: response.serializationDuration,
|
||||
result: .success(filteredImage))
|
||||
|
||||
completion?(response)
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
for (_, _, completion) in responseHandler.operations {
|
||||
DispatchQueue.main.async { completion?(response.mapError { AFIError.alamofireError($0) }) }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 4) Store the response handler for use when the request completes
|
||||
let responseHandler = ResponseHandler(request: request,
|
||||
handlerID: handlerID,
|
||||
receiptID: receiptID,
|
||||
filter: filter,
|
||||
completion: completion)
|
||||
|
||||
self.responseHandlers[urlID] = responseHandler
|
||||
|
||||
// 5) Either start the request or enqueue it depending on the current active request count
|
||||
if self.isActiveRequestCountBelowMaximumLimit() {
|
||||
self.start(request)
|
||||
} else {
|
||||
self.enqueue(request)
|
||||
}
|
||||
}
|
||||
|
||||
if let request = queuedRequest {
|
||||
return RequestReceipt(request: request, receiptID: receiptID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Creates a download request using the internal Alamofire `SessionManager` instance for each specified URL request.
|
||||
///
|
||||
/// For each request, if the same download request is already in the queue or currently being downloaded, the
|
||||
/// filter and completion handler are appended to the already existing request. Once the request completes, all
|
||||
/// filters and completion handlers attached to the request are executed in the order they were added.
|
||||
/// Additionally, any filters attached to the request with the same identifiers are only executed once. The
|
||||
/// resulting image is then passed into each completion handler paired with the filter.
|
||||
///
|
||||
/// You should not attempt to directly cancel any of the `request`s inside the request receipts array since other
|
||||
/// callers may be relying on the completion of that request. Instead, you should call
|
||||
/// `cancelRequestForRequestReceipt` with the returned request receipt to allow the `ImageDownloader` to optimize
|
||||
/// the cancellation on behalf of all active callers.
|
||||
///
|
||||
/// - parameter urlRequests: The URL requests.
|
||||
/// - parameter filter The image filter to apply to the image after each download is complete.
|
||||
/// - parameter progress: The closure to be executed periodically during the lifecycle of the request. Defaults
|
||||
/// to `nil`.
|
||||
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue.
|
||||
/// - parameter completion: The closure called when each download request is complete.
|
||||
///
|
||||
/// - returns: The request receipts for the download requests if available. If an image is stored in the image
|
||||
/// cache and the URL request cache policy allows the cache to be used, a receipt will not be returned
|
||||
/// for that request.
|
||||
@discardableResult
|
||||
open func download(_ urlRequests: [URLRequestConvertible],
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: CompletionHandler? = nil)
|
||||
-> [RequestReceipt] {
|
||||
urlRequests.compactMap {
|
||||
download($0, filter: filter, progress: progress, progressQueue: progressQueue, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancels the request contained inside the receipt calls the completion handler with a request cancelled error.
|
||||
///
|
||||
/// - Parameter requestReceipt: The request receipt to cancel.
|
||||
open func cancelRequest(with requestReceipt: RequestReceipt) {
|
||||
synchronizationQueue.sync {
|
||||
let urlID = ImageDownloader.urlIdentifier(for: requestReceipt.request.convertible)
|
||||
guard let responseHandler = self.responseHandlers[urlID] else { return }
|
||||
|
||||
let index = responseHandler.operations.firstIndex { $0.receiptID == requestReceipt.receiptID }
|
||||
|
||||
if let index = index {
|
||||
let operation = responseHandler.operations.remove(at: index)
|
||||
|
||||
let response: AFIDataResponse<Image> = {
|
||||
let urlRequest = requestReceipt.request.request
|
||||
let error = AFIError.requestCancelled
|
||||
|
||||
return DataResponse(request: urlRequest,
|
||||
response: nil,
|
||||
data: nil,
|
||||
metrics: nil,
|
||||
serializationDuration: 0.0,
|
||||
result: .failure(error))
|
||||
}()
|
||||
|
||||
DispatchQueue.main.async { operation.completion?(response) }
|
||||
}
|
||||
|
||||
if responseHandler.operations.isEmpty {
|
||||
requestReceipt.request.cancel()
|
||||
self.responseHandlers.removeValue(forKey: urlID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Internal - Thread-Safe Request Methods
|
||||
|
||||
func safelyFetchResponseHandler(withURLIdentifier urlIdentifier: String) -> ResponseHandler? {
|
||||
var responseHandler: ResponseHandler?
|
||||
|
||||
synchronizationQueue.sync {
|
||||
responseHandler = self.responseHandlers[urlIdentifier]
|
||||
}
|
||||
|
||||
return responseHandler
|
||||
}
|
||||
|
||||
func safelyRemoveResponseHandler(withURLIdentifier identifier: String) -> ResponseHandler? {
|
||||
var responseHandler: ResponseHandler?
|
||||
|
||||
synchronizationQueue.sync {
|
||||
responseHandler = self.responseHandlers.removeValue(forKey: identifier)
|
||||
}
|
||||
|
||||
return responseHandler
|
||||
}
|
||||
|
||||
func safelyStartNextRequestIfNecessary() {
|
||||
synchronizationQueue.sync {
|
||||
guard self.isActiveRequestCountBelowMaximumLimit() else { return }
|
||||
|
||||
guard let request = self.dequeue() else { return }
|
||||
|
||||
self.start(request)
|
||||
}
|
||||
}
|
||||
|
||||
func safelyDecrementActiveRequestCount() {
|
||||
synchronizationQueue.sync {
|
||||
self.activeRequestCount -= 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Internal - Non Thread-Safe Request Methods
|
||||
|
||||
func start(_ request: Request) {
|
||||
request.resume()
|
||||
activeRequestCount += 1
|
||||
}
|
||||
|
||||
func enqueue(_ request: Request) {
|
||||
switch downloadPrioritization {
|
||||
case .fifo:
|
||||
queuedRequests.append(request)
|
||||
case .lifo:
|
||||
queuedRequests.insert(request, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func dequeue() -> Request? {
|
||||
var request: Request?
|
||||
|
||||
if !queuedRequests.isEmpty {
|
||||
request = queuedRequests.removeFirst()
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
func isActiveRequestCountBelowMaximumLimit() -> Bool {
|
||||
activeRequestCount < maximumActiveDownloads
|
||||
}
|
||||
|
||||
static func urlIdentifier(for urlRequest: URLRequestConvertible) -> String {
|
||||
var urlID: String?
|
||||
|
||||
do {
|
||||
urlID = try urlRequest.asURLRequest().url?.absoluteString
|
||||
} catch {
|
||||
// No-op
|
||||
}
|
||||
|
||||
return urlID ?? ""
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
//
|
||||
// ImageFilter.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
// MARK: ImageFilter
|
||||
|
||||
/// The `ImageFilter` protocol defines properties for filtering an image as well as identification of the filter.
|
||||
public protocol ImageFilter {
|
||||
/// A closure used to create an alternative representation of the given image.
|
||||
var filter: (Image) -> Image { get }
|
||||
|
||||
/// The string used to uniquely identify the filter operation.
|
||||
var identifier: String { get }
|
||||
}
|
||||
|
||||
extension ImageFilter {
|
||||
/// The unique identifier for any `ImageFilter` type.
|
||||
public var identifier: String { "\(type(of: self))" }
|
||||
}
|
||||
|
||||
// MARK: - Sizable
|
||||
|
||||
/// The `Sizable` protocol defines a size property intended for use with `ImageFilter` types.
|
||||
public protocol Sizable {
|
||||
/// The size of the type.
|
||||
var size: CGSize { get }
|
||||
}
|
||||
|
||||
extension ImageFilter where Self: Sizable {
|
||||
/// The unique idenitifier for an `ImageFilter` conforming to the `Sizable` protocol.
|
||||
public var identifier: String {
|
||||
let width = Int64(size.width.rounded())
|
||||
let height = Int64(size.height.rounded())
|
||||
|
||||
return "\(type(of: self))-size:(\(width)x\(height))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Roundable
|
||||
|
||||
/// The `Roundable` protocol defines a radius property intended for use with `ImageFilter` types.
|
||||
public protocol Roundable {
|
||||
/// The radius of the type.
|
||||
var radius: CGFloat { get }
|
||||
}
|
||||
|
||||
extension ImageFilter where Self: Roundable {
|
||||
/// The unique idenitifier for an `ImageFilter` conforming to the `Roundable` protocol.
|
||||
public var identifier: String {
|
||||
let radius = Int64(self.radius.rounded())
|
||||
return "\(type(of: self))-radius:(\(radius))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DynamicImageFilter
|
||||
|
||||
/// The `DynamicImageFilter` class simplifies custom image filter creation by using a trailing closure initializer.
|
||||
public struct DynamicImageFilter: ImageFilter {
|
||||
/// The string used to uniquely identify the image filter operation.
|
||||
public let identifier: String
|
||||
|
||||
/// A closure used to create an alternative representation of the given image.
|
||||
public let filter: (Image) -> Image
|
||||
|
||||
/// Initializes the `DynamicImageFilter` instance with the specified identifier and filter closure.
|
||||
///
|
||||
/// - parameter identifier: The unique identifier of the filter.
|
||||
/// - parameter filter: A closure used to create an alternative representation of the given image.
|
||||
///
|
||||
/// - returns: The new `DynamicImageFilter` instance.
|
||||
public init(_ identifier: String, filter: @escaping (Image) -> Image) {
|
||||
self.identifier = identifier
|
||||
self.filter = filter
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CompositeImageFilter
|
||||
|
||||
/// The `CompositeImageFilter` protocol defines an additional `filters` property to support multiple composite filters.
|
||||
public protocol CompositeImageFilter: ImageFilter {
|
||||
/// The image filters to apply to the image in sequential order.
|
||||
var filters: [ImageFilter] { get }
|
||||
}
|
||||
|
||||
extension CompositeImageFilter {
|
||||
/// The unique idenitifier for any `CompositeImageFilter` type.
|
||||
public var identifier: String {
|
||||
filters.map { $0.identifier }.joined(separator: "_")
|
||||
}
|
||||
|
||||
/// The filter closure for any `CompositeImageFilter` type.
|
||||
public var filter: (Image) -> Image {
|
||||
{ image in
|
||||
self.filters.reduce(image) { $1.filter($0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DynamicCompositeImageFilter
|
||||
|
||||
/// The `DynamicCompositeImageFilter` class is a composite image filter based on a specified array of filters.
|
||||
public struct DynamicCompositeImageFilter: CompositeImageFilter {
|
||||
/// The image filters to apply to the image in sequential order.
|
||||
public let filters: [ImageFilter]
|
||||
|
||||
/// Initializes the `DynamicCompositeImageFilter` instance with the given filters.
|
||||
///
|
||||
/// - parameter filters: The filters taking part in the composite image filter.
|
||||
///
|
||||
/// - returns: The new `DynamicCompositeImageFilter` instance.
|
||||
public init(_ filters: [ImageFilter]) {
|
||||
self.filters = filters
|
||||
}
|
||||
|
||||
/// Initializes the `DynamicCompositeImageFilter` instance with the given filters.
|
||||
///
|
||||
/// - parameter filters: The filters taking part in the composite image filter.
|
||||
///
|
||||
/// - returns: The new `DynamicCompositeImageFilter` instance.
|
||||
public init(_ filters: ImageFilter...) {
|
||||
self.init(filters)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
|
||||
// MARK: - Single Pass Image Filters (iOS, tvOS and watchOS only) -
|
||||
|
||||
/// Scales an image to a specified size.
|
||||
public struct ScaledToSizeFilter: ImageFilter, Sizable {
|
||||
/// The size of the filter.
|
||||
public let size: CGSize
|
||||
|
||||
/// Initializes the `ScaledToSizeFilter` instance with the given size.
|
||||
///
|
||||
/// - parameter size: The size.
|
||||
///
|
||||
/// - returns: The new `ScaledToSizeFilter` instance.
|
||||
public init(size: CGSize) {
|
||||
self.size = size
|
||||
}
|
||||
|
||||
/// The filter closure used to create the modified representation of the given image.
|
||||
public var filter: (Image) -> Image {
|
||||
{ image in
|
||||
image.af.imageScaled(to: self.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Scales an image from the center while maintaining the aspect ratio to fit within a specified size.
|
||||
public struct AspectScaledToFitSizeFilter: ImageFilter, Sizable {
|
||||
/// The size of the filter.
|
||||
public let size: CGSize
|
||||
|
||||
/// Initializes the `AspectScaledToFitSizeFilter` instance with the given size.
|
||||
///
|
||||
/// - parameter size: The size.
|
||||
///
|
||||
/// - returns: The new `AspectScaledToFitSizeFilter` instance.
|
||||
public init(size: CGSize) {
|
||||
self.size = size
|
||||
}
|
||||
|
||||
/// The filter closure used to create the modified representation of the given image.
|
||||
public var filter: (Image) -> Image {
|
||||
{ image in
|
||||
image.af.imageAspectScaled(toFit: self.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Scales an image from the center while maintaining the aspect ratio to fill a specified size. Any pixels that fall
|
||||
/// outside the specified size are clipped.
|
||||
public struct AspectScaledToFillSizeFilter: ImageFilter, Sizable {
|
||||
/// The size of the filter.
|
||||
public let size: CGSize
|
||||
|
||||
/// Initializes the `AspectScaledToFillSizeFilter` instance with the given size.
|
||||
///
|
||||
/// - parameter size: The size.
|
||||
///
|
||||
/// - returns: The new `AspectScaledToFillSizeFilter` instance.
|
||||
public init(size: CGSize) {
|
||||
self.size = size
|
||||
}
|
||||
|
||||
/// The filter closure used to create the modified representation of the given image.
|
||||
public var filter: (Image) -> Image {
|
||||
{ image in
|
||||
image.af.imageAspectScaled(toFill: self.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Rounds the corners of an image to the specified radius.
|
||||
public struct RoundedCornersFilter: ImageFilter, Roundable {
|
||||
/// The radius of the filter.
|
||||
public let radius: CGFloat
|
||||
|
||||
/// Whether to divide the radius by the image scale.
|
||||
public let divideRadiusByImageScale: Bool
|
||||
|
||||
/// Initializes the `RoundedCornersFilter` instance with the given radius.
|
||||
///
|
||||
/// - parameter radius: The radius.
|
||||
/// - parameter divideRadiusByImageScale: Whether to divide the radius by the image scale. Set to `true` when the
|
||||
/// image has the same resolution for all screen scales such as @1x, @2x and
|
||||
/// @3x (i.e. single image from web server). Set to `false` for images loaded
|
||||
/// from an asset catalog with varying resolutions for each screen scale.
|
||||
/// `false` by default.
|
||||
///
|
||||
/// - returns: The new `RoundedCornersFilter` instance.
|
||||
public init(radius: CGFloat, divideRadiusByImageScale: Bool = false) {
|
||||
self.radius = radius
|
||||
self.divideRadiusByImageScale = divideRadiusByImageScale
|
||||
}
|
||||
|
||||
/// The filter closure used to create the modified representation of the given image.
|
||||
public var filter: (Image) -> Image {
|
||||
{ image in
|
||||
image.af.imageRounded(withCornerRadius: self.radius,
|
||||
divideRadiusByImageScale: self.divideRadiusByImageScale)
|
||||
}
|
||||
}
|
||||
|
||||
/// The unique idenitifier for an `ImageFilter` conforming to the `Roundable` protocol.
|
||||
public var identifier: String {
|
||||
let radius = Int64(self.radius.rounded())
|
||||
return "\(type(of: self))-radius:(\(radius))-divided:(\(divideRadiusByImageScale))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Rounds the corners of an image into a circle.
|
||||
public struct CircleFilter: ImageFilter {
|
||||
/// Initializes the `CircleFilter` instance.
|
||||
///
|
||||
/// - returns: The new `CircleFilter` instance.
|
||||
public init() {}
|
||||
|
||||
/// The filter closure used to create the modified representation of the given image.
|
||||
public var filter: (Image) -> Image {
|
||||
{ image in
|
||||
image.af.imageRoundedIntoCircle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
|
||||
/// The `CoreImageFilter` protocol defines `parameters`, `filterName` properties used by CoreImage.
|
||||
public protocol CoreImageFilter: ImageFilter {
|
||||
/// The filter name of the CoreImage filter.
|
||||
var filterName: String { get }
|
||||
|
||||
/// The image filter parameters passed to CoreImage.
|
||||
var parameters: [String: Any] { get }
|
||||
}
|
||||
|
||||
extension ImageFilter where Self: CoreImageFilter {
|
||||
/// The filter closure used to create the modified representation of the given image.
|
||||
public var filter: (Image) -> Image {
|
||||
{ image in
|
||||
image.af.imageFiltered(withCoreImageFilter: self.filterName, parameters: self.parameters) ?? image
|
||||
}
|
||||
}
|
||||
|
||||
/// The unique idenitifier for an `ImageFilter` conforming to the `CoreImageFilter` protocol.
|
||||
public var identifier: String { "\(type(of: self))-parameters:(\(parameters))" }
|
||||
}
|
||||
|
||||
/// Blurs an image using a `CIGaussianBlur` filter with the specified blur radius.
|
||||
public struct BlurFilter: ImageFilter, CoreImageFilter {
|
||||
/// The filter name.
|
||||
public let filterName = "CIGaussianBlur"
|
||||
|
||||
/// The image filter parameters passed to CoreImage.
|
||||
public let parameters: [String: Any]
|
||||
|
||||
/// Initializes the `BlurFilter` instance with the given blur radius.
|
||||
///
|
||||
/// - parameter blurRadius: The blur radius.
|
||||
///
|
||||
/// - returns: The new `BlurFilter` instance.
|
||||
public init(blurRadius: UInt = 10) {
|
||||
parameters = ["inputRadius": blurRadius]
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - Composite Image Filters (iOS, tvOS and watchOS only) -
|
||||
|
||||
/// Scales an image to a specified size, then rounds the corners to the specified radius.
|
||||
public struct ScaledToSizeWithRoundedCornersFilter: CompositeImageFilter {
|
||||
/// Initializes the `ScaledToSizeWithRoundedCornersFilter` instance with the given size and radius.
|
||||
///
|
||||
/// - parameter size: The size.
|
||||
/// - parameter radius: The radius.
|
||||
/// - parameter divideRadiusByImageScale: Whether to divide the radius by the image scale. Set to `true` when the
|
||||
/// image has the same resolution for all screen scales such as @1x, @2x and
|
||||
/// @3x (i.e. single image from web server). Set to `false` for images loaded
|
||||
/// from an asset catalog with varying resolutions for each screen scale.
|
||||
/// `false` by default.
|
||||
///
|
||||
/// - returns: The new `ScaledToSizeWithRoundedCornersFilter` instance.
|
||||
public init(size: CGSize, radius: CGFloat, divideRadiusByImageScale: Bool = false) {
|
||||
filters = [ScaledToSizeFilter(size: size),
|
||||
RoundedCornersFilter(radius: radius, divideRadiusByImageScale: divideRadiusByImageScale)]
|
||||
}
|
||||
|
||||
/// The image filters to apply to the image in sequential order.
|
||||
public let filters: [ImageFilter]
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Scales an image from the center while maintaining the aspect ratio to fit within a specified size, then rounds the
|
||||
/// corners to the specified radius.
|
||||
public struct AspectScaledToFillSizeWithRoundedCornersFilter: CompositeImageFilter {
|
||||
/// Initializes the `AspectScaledToFillSizeWithRoundedCornersFilter` instance with the given size and radius.
|
||||
///
|
||||
/// - parameter size: The size.
|
||||
/// - parameter radius: The radius.
|
||||
/// - parameter divideRadiusByImageScale: Whether to divide the radius by the image scale. Set to `true` when the
|
||||
/// image has the same resolution for all screen scales such as @1x, @2x and
|
||||
/// @3x (i.e. single image from web server). Set to `false` for images loaded
|
||||
/// from an asset catalog with varying resolutions for each screen scale.
|
||||
/// `false` by default.
|
||||
///
|
||||
/// - returns: The new `AspectScaledToFillSizeWithRoundedCornersFilter` instance.
|
||||
public init(size: CGSize, radius: CGFloat, divideRadiusByImageScale: Bool = false) {
|
||||
filters = [AspectScaledToFillSizeFilter(size: size),
|
||||
RoundedCornersFilter(radius: radius, divideRadiusByImageScale: divideRadiusByImageScale)]
|
||||
}
|
||||
|
||||
/// The image filters to apply to the image in sequential order.
|
||||
public let filters: [ImageFilter]
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Scales an image to a specified size, then rounds the corners into a circle.
|
||||
public struct ScaledToSizeCircleFilter: CompositeImageFilter {
|
||||
/// Initializes the `ScaledToSizeCircleFilter` instance with the given size.
|
||||
///
|
||||
/// - parameter size: The size.
|
||||
///
|
||||
/// - returns: The new `ScaledToSizeCircleFilter` instance.
|
||||
public init(size: CGSize) {
|
||||
filters = [ScaledToSizeFilter(size: size), CircleFilter()]
|
||||
}
|
||||
|
||||
/// The image filters to apply to the image in sequential order.
|
||||
public let filters: [ImageFilter]
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Scales an image from the center while maintaining the aspect ratio to fit within a specified size, then rounds the
|
||||
/// corners into a circle.
|
||||
public struct AspectScaledToFillSizeCircleFilter: CompositeImageFilter {
|
||||
/// Initializes the `AspectScaledToFillSizeCircleFilter` instance with the given size.
|
||||
///
|
||||
/// - parameter size: The size.
|
||||
///
|
||||
/// - returns: The new `AspectScaledToFillSizeCircleFilter` instance.
|
||||
public init(size: CGSize) {
|
||||
filters = [AspectScaledToFillSizeFilter(size: size), CircleFilter()]
|
||||
}
|
||||
|
||||
/// The image filters to apply to the image in sequential order.
|
||||
public let filters: [ImageFilter]
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// Request+AlamofireImage.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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 Alamofire
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
#elseif os(watchOS)
|
||||
import UIKit
|
||||
import WatchKit
|
||||
#elseif os(macOS)
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
public final class ImageResponseSerializer: ResponseSerializer {
|
||||
// MARK: Properties
|
||||
|
||||
public static var deviceScreenScale: CGFloat { DataRequest.imageScale }
|
||||
|
||||
public let imageScale: CGFloat
|
||||
public let inflateResponseImage: Bool
|
||||
public let emptyResponseCodes: Set<Int>
|
||||
public let emptyRequestMethods: Set<HTTPMethod>
|
||||
|
||||
static var acceptableImageContentTypes: Set<String> = {
|
||||
var contentTypes: Set<String> = ["application/octet-stream",
|
||||
"image/tiff",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"image/jp2",
|
||||
"image/gif",
|
||||
"image/png",
|
||||
"image/ico",
|
||||
"image/x-icon",
|
||||
"image/bmp",
|
||||
"image/x-bmp",
|
||||
"image/x-xbitmap",
|
||||
"image/x-ms-bmp",
|
||||
"image/x-win-bitmap"]
|
||||
|
||||
#if os(macOS) || os(iOS) // No WebP support on tvOS or watchOS.
|
||||
if #available(macOS 11, iOS 14, *) {
|
||||
contentTypes.formUnion(["image/webp"])
|
||||
}
|
||||
#endif
|
||||
|
||||
if #available(macOS 10.13, iOS 11, tvOS 11, watchOS 4, *) {
|
||||
contentTypes.formUnion(["image/heic", "image/heif"])
|
||||
}
|
||||
|
||||
return contentTypes
|
||||
}()
|
||||
|
||||
static let streamImageInitialBytePattern = Data([255, 216]) // 0xffd8
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
public init(imageScale: CGFloat = ImageResponseSerializer.deviceScreenScale,
|
||||
inflateResponseImage: Bool = true,
|
||||
emptyResponseCodes: Set<Int> = ImageResponseSerializer.defaultEmptyResponseCodes,
|
||||
emptyRequestMethods: Set<HTTPMethod> = ImageResponseSerializer.defaultEmptyRequestMethods) {
|
||||
self.imageScale = imageScale
|
||||
self.inflateResponseImage = inflateResponseImage
|
||||
self.emptyResponseCodes = emptyResponseCodes
|
||||
self.emptyRequestMethods = emptyRequestMethods
|
||||
}
|
||||
|
||||
// MARK: Serialization
|
||||
|
||||
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Image {
|
||||
guard error == nil else { throw error! }
|
||||
|
||||
guard let data = data, !data.isEmpty else {
|
||||
guard emptyResponseAllowed(forRequest: request, response: response) else {
|
||||
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)
|
||||
}
|
||||
|
||||
print("Returning empty image!")
|
||||
return Image()
|
||||
}
|
||||
|
||||
try validateContentType(for: request, response: response)
|
||||
let image = try serializeImage(from: data)
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
public func serializeImage(from data: Data) throws -> Image {
|
||||
guard !data.isEmpty else {
|
||||
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)
|
||||
}
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
guard let image = UIImage.af.threadSafeImage(with: data, scale: imageScale) else {
|
||||
throw AFIError.imageSerializationFailed
|
||||
}
|
||||
|
||||
if inflateResponseImage { image.af.inflate() }
|
||||
#elseif os(macOS)
|
||||
guard let bitmapImage = NSBitmapImageRep(data: data) else {
|
||||
throw AFIError.imageSerializationFailed
|
||||
}
|
||||
|
||||
let image = NSImage(size: NSSize(width: bitmapImage.pixelsWide, height: bitmapImage.pixelsHigh))
|
||||
image.addRepresentation(bitmapImage)
|
||||
#endif
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
// MARK: Content Type Validation
|
||||
|
||||
/// Adds the content types specified to the list of acceptable images content types for validation.
|
||||
///
|
||||
/// - parameter contentTypes: The additional content types.
|
||||
public class func addAcceptableImageContentTypes(_ contentTypes: Set<String>) {
|
||||
ImageResponseSerializer.acceptableImageContentTypes.formUnion(contentTypes)
|
||||
}
|
||||
|
||||
public func validateContentType(for request: URLRequest?, response: HTTPURLResponse?) throws {
|
||||
if let url = request?.url, url.isFileURL { return }
|
||||
|
||||
guard let mimeType = response?.mimeType else {
|
||||
let contentTypes = Array(ImageResponseSerializer.acceptableImageContentTypes)
|
||||
throw AFError.responseValidationFailed(reason: .missingContentType(acceptableContentTypes: contentTypes))
|
||||
}
|
||||
|
||||
guard ImageResponseSerializer.acceptableImageContentTypes.contains(mimeType) else {
|
||||
let contentTypes = Array(ImageResponseSerializer.acceptableImageContentTypes)
|
||||
|
||||
throw AFError.responseValidationFailed(
|
||||
reason: .unacceptableContentType(acceptableContentTypes: contentTypes, responseContentType: mimeType)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Scale
|
||||
|
||||
extension DataRequest {
|
||||
public class var imageScale: CGFloat {
|
||||
#if os(iOS) || os(tvOS)
|
||||
return UIScreen.main.scale
|
||||
#elseif os(watchOS)
|
||||
return WKInterfaceDevice.current().screenScale
|
||||
#elseif os(macOS)
|
||||
return 1.0
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iOS, tvOS, and watchOS
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
|
||||
extension DataRequest {
|
||||
/// Adds a response handler to be called once the request has finished.
|
||||
///
|
||||
/// - parameter imageScale: The scale factor used when interpreting the image data to construct
|
||||
/// `responseImage`. Specifying a scale factor of 1.0 results in an image whose
|
||||
/// size matches the pixel-based dimensions of the image. Applying a different
|
||||
/// scale factor changes the size of the image as reported by the size property.
|
||||
/// This is set to the value of scale of the main screen by default, which
|
||||
/// automatically scales images for retina displays, for instance.
|
||||
/// `Screen.scale` by default.
|
||||
/// - parameter inflateResponseImage: Whether to automatically inflate response image data for compressed formats
|
||||
/// (such as PNG or JPEG). Enabling this can significantly improve drawing
|
||||
/// performance as it allows a bitmap representation to be constructed in the
|
||||
/// background rather than on the main thread. `true` by default.
|
||||
/// - parameter queue: The queue on which the completion handler is dispatched. `.main` by default.
|
||||
/// - parameter completionHandler: A closure to be executed once the request has finished. The closure takes 4
|
||||
/// arguments: the URL request, the URL response, if one was received, the image,
|
||||
/// if one could be created from the URL response and data, and any error produced
|
||||
/// while creating the image.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responseImage(imageScale: CGFloat = DataRequest.imageScale,
|
||||
inflateResponseImage: Bool = true,
|
||||
queue: DispatchQueue = .main,
|
||||
completionHandler: @escaping (AFDataResponse<Image>) -> Void)
|
||||
-> Self {
|
||||
response(queue: queue,
|
||||
responseSerializer: ImageResponseSerializer(imageScale: imageScale,
|
||||
inflateResponseImage: inflateResponseImage),
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - macOS
|
||||
|
||||
#elseif os(macOS)
|
||||
|
||||
extension DataRequest {
|
||||
/// Adds a response handler to be called once the request has finished.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - queue: The queue on which the completion handler is dispatched. `.main` by default.
|
||||
/// - completionHandler: A closure to be executed once the request has finished. The closure takes 4 arguments:
|
||||
/// the URL request, the URL response, if one was received, the image, if one could be
|
||||
/// created from the URL response and data, and any error produced while creating the image.
|
||||
///
|
||||
/// - returns: The request.
|
||||
@discardableResult
|
||||
public func responseImage(queue: DispatchQueue = .main,
|
||||
completionHandler: @escaping (AFDataResponse<Image>) -> Void)
|
||||
-> Self {
|
||||
response(queue: queue,
|
||||
responseSerializer: ImageResponseSerializer(inflateResponseImage: false),
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,617 @@
|
||||
//
|
||||
// UIButton+AlamofireImage.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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 Alamofire
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
|
||||
import UIKit
|
||||
|
||||
public typealias ControlState = UIControl.State
|
||||
|
||||
extension UIButton: AlamofireExtended {}
|
||||
extension AlamofireExtension where ExtendedType: UIButton {
|
||||
// MARK: - Properties
|
||||
|
||||
/// The instance image downloader used to download all images. If this property is `nil`, the `UIButton` will
|
||||
/// fallback on the `sharedImageDownloader` for all downloads. The most common use case for needing to use a
|
||||
/// custom instance image downloader is when images are behind different basic auth credentials.
|
||||
public var imageDownloader: ImageDownloader? {
|
||||
get {
|
||||
objc_getAssociatedObject(type, &AssociatedKeys.imageDownloader) as? ImageDownloader
|
||||
}
|
||||
nonmutating set {
|
||||
objc_setAssociatedObject(type, &AssociatedKeys.imageDownloader, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
/// The shared image downloader used to download all images. By default, this is the default `ImageDownloader`
|
||||
/// instance backed with an `AutoPurgingImageCache` which automatically evicts images from the cache when the memory
|
||||
/// capacity is reached or memory warning notifications occur. The shared image downloader is only used if the
|
||||
/// `imageDownloader` is `nil`.
|
||||
public static var sharedImageDownloader: ImageDownloader {
|
||||
get {
|
||||
guard let
|
||||
downloader = objc_getAssociatedObject(UIButton.self, &AssociatedKeys.sharedImageDownloader) as? ImageDownloader
|
||||
else { return ImageDownloader.default }
|
||||
|
||||
return downloader
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(UIButton.self, &AssociatedKeys.sharedImageDownloader, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
private var imageRequestReceipts: [UInt: RequestReceipt] {
|
||||
get {
|
||||
guard let
|
||||
receipts = objc_getAssociatedObject(type, &AssociatedKeys.imageReceipts) as? [UInt: RequestReceipt]
|
||||
else { return [:] }
|
||||
|
||||
return receipts
|
||||
}
|
||||
nonmutating set {
|
||||
objc_setAssociatedObject(type, &AssociatedKeys.imageReceipts, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundImageRequestReceipts: [UInt: RequestReceipt] {
|
||||
get {
|
||||
guard let
|
||||
receipts = objc_getAssociatedObject(type, &AssociatedKeys.backgroundImageReceipts) as? [UInt: RequestReceipt]
|
||||
else { return [:] }
|
||||
|
||||
return receipts
|
||||
}
|
||||
nonmutating set {
|
||||
objc_setAssociatedObject(type, &AssociatedKeys.backgroundImageReceipts, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Downloads
|
||||
|
||||
/// Asynchronously downloads an image from the specified URL and sets it once the request is finished.
|
||||
///
|
||||
/// If the image is cached locally, the image is set immediately. Otherwise the specified placeholder image will be
|
||||
/// set immediately, and then the remote image will be set once the image request is finished.
|
||||
///
|
||||
/// - parameter state: The control state of the button to set the image on.
|
||||
/// - parameter url: The URL used for your image request.
|
||||
/// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults to `nil`.
|
||||
/// - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the
|
||||
/// image will not change its image until the image request finishes. Defaults
|
||||
/// to `nil`.
|
||||
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`. Defaults
|
||||
/// to `nil` which will fall back to the instance `imageResponseSerializer` set on
|
||||
/// the `ImageDownloader`.
|
||||
/// - parameter filter: The image filter applied to the image after the image request is finished.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progress: The closure to be executed periodically during the lifecycle of the request.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue.
|
||||
/// - parameter completion: A closure to be executed when the image request finishes. The closure takes a
|
||||
/// single response value containing either the image or the error that occurred. If
|
||||
/// the image was returned from the image cache, the response will be `nil`. Defaults
|
||||
/// to `nil`.
|
||||
public func setImage(for state: ControlState,
|
||||
url: URL,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
setImage(for: state,
|
||||
urlRequest: urlRequest(with: url),
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Asynchronously downloads an image from the specified URL and sets it once the request is finished.
|
||||
///
|
||||
/// If the image is cached locally, the image is set immediately. Otherwise the specified placeholder image will be
|
||||
/// set immediately, and then the remote image will be set once the image request is finished.
|
||||
///
|
||||
/// - parameter state: The control state of the button to set the image on.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
/// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults to `nil`.
|
||||
/// - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the
|
||||
/// image will not change its image until the image request finishes. Defaults
|
||||
/// to `nil`.
|
||||
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`. Defaults
|
||||
/// to `nil` which will fall back to the instance `imageResponseSerializer` set on
|
||||
/// the `ImageDownloader`.
|
||||
/// - parameter filter: The image filter applied to the image after the image request is finished.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progress: The closure to be executed periodically during the lifecycle of the request.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue.
|
||||
/// - parameter completion: A closure to be executed when the image request finishes. The closure takes a
|
||||
/// single response value containing either the image or the error that occurred. If
|
||||
/// the image was returned from the image cache, the response will be `nil`. Defaults
|
||||
/// to `nil`.
|
||||
public func setImage(for state: ControlState,
|
||||
urlRequest: URLRequestConvertible,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
guard !isImageURLRequest(urlRequest, equalToActiveRequestURLForState: state) else {
|
||||
let response = AFIDataResponse<UIImage>(request: nil,
|
||||
response: nil,
|
||||
data: nil,
|
||||
metrics: nil,
|
||||
serializationDuration: 0.0,
|
||||
result: .failure(AFIError.requestCancelled))
|
||||
|
||||
completion?(response)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cancelImageRequest(for: state)
|
||||
|
||||
let imageDownloader = self.imageDownloader ?? UIButton.af.sharedImageDownloader
|
||||
let imageCache = imageDownloader.imageCache
|
||||
|
||||
// Use the image from the image cache if it exists
|
||||
if let request = urlRequest.urlRequest {
|
||||
let cachedImage: Image?
|
||||
|
||||
if let cacheKey = cacheKey {
|
||||
cachedImage = imageCache?.image(withIdentifier: cacheKey)
|
||||
} else {
|
||||
cachedImage = imageCache?.image(for: request, withIdentifier: filter?.identifier)
|
||||
}
|
||||
|
||||
if let image = cachedImage {
|
||||
let response = AFIDataResponse<UIImage>(request: urlRequest.urlRequest,
|
||||
response: nil,
|
||||
data: nil,
|
||||
metrics: nil,
|
||||
serializationDuration: 0.0,
|
||||
result: .success(image))
|
||||
|
||||
type.setImage(image, for: state)
|
||||
completion?(response)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set the placeholder since we're going to have to download
|
||||
if let placeholderImage = placeholderImage { type.setImage(placeholderImage, for: state) }
|
||||
|
||||
// Generate a unique download id to check whether the active request has changed while downloading
|
||||
let downloadID = UUID().uuidString
|
||||
|
||||
// Weakify the button to allow it to go out-of-memory while download is running if deallocated
|
||||
weak var button = type
|
||||
|
||||
// Download the image, then set the image for the control state
|
||||
let requestReceipt = imageDownloader.download(urlRequest,
|
||||
cacheKey: cacheKey,
|
||||
receiptID: downloadID,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: { response in
|
||||
guard
|
||||
let strongSelf = button?.af,
|
||||
strongSelf.isImageURLRequest(response.request, equalToActiveRequestURLForState: state) &&
|
||||
strongSelf.imageRequestReceipt(for: state)?.receiptID == downloadID
|
||||
else {
|
||||
completion?(response)
|
||||
return
|
||||
}
|
||||
|
||||
if case let .success(image) = response.result {
|
||||
strongSelf.type.setImage(image, for: state)
|
||||
}
|
||||
|
||||
strongSelf.setImageRequestReceipt(nil, for: state)
|
||||
|
||||
completion?(response)
|
||||
})
|
||||
|
||||
setImageRequestReceipt(requestReceipt, for: state)
|
||||
}
|
||||
|
||||
/// Cancels the active download request for the image, if one exists.
|
||||
public func cancelImageRequest(for state: ControlState) {
|
||||
guard let receipt = imageRequestReceipt(for: state) else { return }
|
||||
|
||||
let imageDownloader = self.imageDownloader ?? UIButton.af.sharedImageDownloader
|
||||
imageDownloader.cancelRequest(with: receipt)
|
||||
|
||||
setImageRequestReceipt(nil, for: state)
|
||||
}
|
||||
|
||||
// MARK: - Background Image Downloads
|
||||
|
||||
/// Asynchronously downloads an image from the specified URL and sets it once the request is finished.
|
||||
///
|
||||
/// If the image is cached locally, the image is set immediately. Otherwise the specified placeholder image will be
|
||||
/// set immediately, and then the remote image will be set once the image request is finished.
|
||||
///
|
||||
/// - parameter state: The control state of the button to set the image on.
|
||||
/// - parameter url: The URL used for the image request.
|
||||
/// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults to `nil`.
|
||||
/// - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the
|
||||
/// background image will not change its image until the image request finishes.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`. Defaults
|
||||
/// to `nil` which will fall back to the instance `imageResponseSerializer` set on
|
||||
/// the `ImageDownloader`.
|
||||
/// - parameter filter: The image filter applied to the image after the image request is finished.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progress: The closure to be executed periodically during the lifecycle of the request.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue.
|
||||
/// - parameter completion: A closure to be executed when the image request finishes. The closure takes a
|
||||
/// single response value containing either the image or the error that occurred. If
|
||||
/// the image was returned from the image cache, the response will be `nil`. Defaults
|
||||
/// to `nil`.
|
||||
public func setBackgroundImage(for state: ControlState,
|
||||
url: URL,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
setBackgroundImage(for: state,
|
||||
urlRequest: urlRequest(with: url),
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Asynchronously downloads an image from the specified URL request and sets it once the request is finished.
|
||||
///
|
||||
/// If the image is cached locally, the image is set immediately. Otherwise the specified placeholder image will be
|
||||
/// set immediately, and then the remote image will be set once the image request is finished.
|
||||
///
|
||||
/// - parameter state: The control state of the button to set the image on.
|
||||
/// - parameter urlRequest: The URL request.
|
||||
/// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults to `nil`.
|
||||
/// - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the
|
||||
/// background image will not change its image until the image request finishes.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`. Defaults
|
||||
/// to `nil` which will fall back to the instance `imageResponseSerializer` set on
|
||||
/// the `ImageDownloader`.
|
||||
/// - parameter filter: The image filter applied to the image after the image request is finished.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progress: The closure to be executed periodically during the lifecycle of the request.
|
||||
/// Defaults to `nil`.
|
||||
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the main queue.
|
||||
/// - parameter completion: A closure to be executed when the image request finishes. The closure takes a
|
||||
/// single response value containing either the image or the error that occurred. If
|
||||
/// the image was returned from the image cache, the response will be `nil`. Defaults
|
||||
/// to `nil`.
|
||||
public func setBackgroundImage(for state: ControlState,
|
||||
urlRequest: URLRequestConvertible,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
guard !isImageURLRequest(urlRequest, equalToActiveRequestURLForState: state) else {
|
||||
let response = AFIDataResponse<UIImage>(request: nil,
|
||||
response: nil,
|
||||
data: nil,
|
||||
metrics: nil,
|
||||
serializationDuration: 0.0,
|
||||
result: .failure(AFIError.requestCancelled))
|
||||
|
||||
completion?(response)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cancelBackgroundImageRequest(for: state)
|
||||
|
||||
let imageDownloader = self.imageDownloader ?? UIButton.af.sharedImageDownloader
|
||||
let imageCache = imageDownloader.imageCache
|
||||
|
||||
// Use the image from the image cache if it exists
|
||||
if let request = urlRequest.urlRequest {
|
||||
let cachedImage: Image?
|
||||
|
||||
if let cacheKey = cacheKey {
|
||||
cachedImage = imageCache?.image(withIdentifier: cacheKey)
|
||||
} else {
|
||||
cachedImage = imageCache?.image(for: request, withIdentifier: filter?.identifier)
|
||||
}
|
||||
|
||||
if let image = cachedImage {
|
||||
let response = AFIDataResponse<UIImage>(request: urlRequest.urlRequest,
|
||||
response: nil,
|
||||
data: nil,
|
||||
metrics: nil,
|
||||
serializationDuration: 0.0,
|
||||
result: .success(image))
|
||||
|
||||
type.setBackgroundImage(image, for: state)
|
||||
completion?(response)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set the placeholder since we're going to have to download
|
||||
if let placeholderImage = placeholderImage { type.setBackgroundImage(placeholderImage, for: state) }
|
||||
|
||||
// Generate a unique download id to check whether the active request has changed while downloading
|
||||
let downloadID = UUID().uuidString
|
||||
|
||||
// Weakify the button to allow it to go out-of-memory while download is running if deallocated
|
||||
weak var button = type
|
||||
|
||||
// Download the image, then set the image for the control state
|
||||
let requestReceipt = imageDownloader.download(urlRequest,
|
||||
cacheKey: cacheKey,
|
||||
receiptID: downloadID,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: { response in
|
||||
guard
|
||||
let strongSelf = button?.af,
|
||||
strongSelf.isBackgroundImageURLRequest(response.request, equalToActiveRequestURLForState: state) &&
|
||||
strongSelf.backgroundImageRequestReceipt(for: state)?.receiptID == downloadID
|
||||
else {
|
||||
completion?(response)
|
||||
return
|
||||
}
|
||||
|
||||
if case let .success(image) = response.result {
|
||||
strongSelf.type.setBackgroundImage(image, for: state)
|
||||
}
|
||||
|
||||
strongSelf.setBackgroundImageRequestReceipt(nil, for: state)
|
||||
|
||||
completion?(response)
|
||||
})
|
||||
|
||||
setBackgroundImageRequestReceipt(requestReceipt, for: state)
|
||||
}
|
||||
|
||||
/// Cancels the active download request for the background image, if one exists.
|
||||
public func cancelBackgroundImageRequest(for state: ControlState) {
|
||||
guard let receipt = backgroundImageRequestReceipt(for: state) else { return }
|
||||
|
||||
let imageDownloader = self.imageDownloader ?? UIButton.af.sharedImageDownloader
|
||||
imageDownloader.cancelRequest(with: receipt)
|
||||
|
||||
setBackgroundImageRequestReceipt(nil, for: state)
|
||||
}
|
||||
|
||||
// MARK: - Internal - Image Request Receipts
|
||||
|
||||
func imageRequestReceipt(for state: ControlState) -> RequestReceipt? {
|
||||
guard let receipt = imageRequestReceipts[state.rawValue] else { return nil }
|
||||
return receipt
|
||||
}
|
||||
|
||||
func setImageRequestReceipt(_ receipt: RequestReceipt?, for state: ControlState) {
|
||||
var receipts = imageRequestReceipts
|
||||
receipts[state.rawValue] = receipt
|
||||
|
||||
imageRequestReceipts = receipts
|
||||
}
|
||||
|
||||
// MARK: - Internal - Background Image Request Receipts
|
||||
|
||||
func backgroundImageRequestReceipt(for state: ControlState) -> RequestReceipt? {
|
||||
guard let receipt = backgroundImageRequestReceipts[state.rawValue] else { return nil }
|
||||
return receipt
|
||||
}
|
||||
|
||||
func setBackgroundImageRequestReceipt(_ receipt: RequestReceipt?, for state: ControlState) {
|
||||
var receipts = backgroundImageRequestReceipts
|
||||
receipts[state.rawValue] = receipt
|
||||
|
||||
backgroundImageRequestReceipts = receipts
|
||||
}
|
||||
|
||||
// MARK: - Private - URL Request Helpers
|
||||
|
||||
private func isImageURLRequest(_ urlRequest: URLRequestConvertible?,
|
||||
equalToActiveRequestURLForState state: ControlState)
|
||||
-> Bool {
|
||||
if
|
||||
let currentURL = imageRequestReceipt(for: state)?.request.task?.originalRequest?.url,
|
||||
let requestURL = urlRequest?.urlRequest?.url,
|
||||
currentURL == requestURL {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func isBackgroundImageURLRequest(_ urlRequest: URLRequestConvertible?,
|
||||
equalToActiveRequestURLForState state: ControlState)
|
||||
-> Bool {
|
||||
if
|
||||
let currentRequestURL = backgroundImageRequestReceipt(for: state)?.request.task?.originalRequest?.url,
|
||||
let requestURL = urlRequest?.urlRequest?.url,
|
||||
currentRequestURL == requestURL {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func urlRequest(with url: URL) -> URLRequest {
|
||||
var urlRequest = URLRequest(url: url)
|
||||
|
||||
for mimeType in ImageResponseSerializer.acceptableImageContentTypes.sorted() {
|
||||
urlRequest.addValue(mimeType, forHTTPHeaderField: "Accept")
|
||||
}
|
||||
|
||||
return urlRequest
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Deprecated
|
||||
|
||||
extension UIButton {
|
||||
@available(*, deprecated, message: "Replaced by `button.af.imageDownloader`")
|
||||
public var af_imageDownloader: ImageDownloader? {
|
||||
get { af.imageDownloader }
|
||||
set { af.imageDownloader = newValue }
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `button.af.sharedImageDownloader`")
|
||||
public class var af_sharedImageDownloader: ImageDownloader {
|
||||
get { af.sharedImageDownloader }
|
||||
set { af.sharedImageDownloader = newValue }
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `button.af.sharedImageDownloader`")
|
||||
public func af_setImage(for state: ControlState,
|
||||
url: URL,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
af.setImage(for: state,
|
||||
url: url,
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `button.af.sharedImageDownloader`")
|
||||
public func af_setImage(for state: ControlState,
|
||||
urlRequest: URLRequestConvertible,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
af.setImage(for: state,
|
||||
urlRequest: urlRequest,
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Cancels the active download request for the image, if one exists.
|
||||
public func af_cancelImageRequest(for state: ControlState) {
|
||||
af.cancelImageRequest(for: state)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `button.af.sharedImageDownloader`")
|
||||
public func af_setBackgroundImage(for state: ControlState,
|
||||
url: URL,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
af.setBackgroundImage(for: state,
|
||||
url: url,
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `button.af.sharedImageDownloader`")
|
||||
public func af_setBackgroundImage(for state: ControlState,
|
||||
urlRequest: URLRequestConvertible,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
af.setBackgroundImage(for: state,
|
||||
urlRequest: urlRequest,
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Cancels the active download request for the background image, if one exists.
|
||||
public func af_cancelBackgroundImageRequest(for state: ControlState) {
|
||||
af.cancelBackgroundImageRequest(for: state)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private - AssociatedKeys
|
||||
|
||||
private enum AssociatedKeys {
|
||||
static var imageDownloader = "UIButton.af.imageDownloader"
|
||||
static var sharedImageDownloader = "UIButton.af.sharedImageDownloader"
|
||||
static var imageReceipts = "UIButton.af.imageReceipts"
|
||||
static var backgroundImageReceipts = "UIButton.af.backgroundImageReceipts"
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,395 @@
|
||||
//
|
||||
// UIImage+AlamofireImage.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
|
||||
import Alamofire
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
private let lock = NSLock()
|
||||
|
||||
extension UIImage: AlamofireExtended {}
|
||||
extension AlamofireExtension where ExtendedType: UIImage {
|
||||
/// Initializes and returns the image object with the specified data in a thread-safe manner.
|
||||
///
|
||||
/// It has been reported that there are thread-safety issues when initializing large amounts of images
|
||||
/// simultaneously. In the event of these issues occurring, this method can be used in place of
|
||||
/// the `init?(data:)` method.
|
||||
///
|
||||
/// - parameter data: The data object containing the image data.
|
||||
///
|
||||
/// - returns: An initialized `UIImage` object, or `nil` if the method failed.
|
||||
public static func threadSafeImage(with data: Data) -> UIImage? {
|
||||
lock.lock()
|
||||
let image = UIImage(data: data)
|
||||
lock.unlock()
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
/// Initializes and returns the image object with the specified data and scale in a thread-safe manner.
|
||||
///
|
||||
/// It has been reported that there are thread-safety issues when initializing large amounts of images
|
||||
/// simultaneously. In the event of these issues occurring, this method can be used in place of
|
||||
/// the `init?(data:scale:)` method.
|
||||
///
|
||||
/// - parameter data: The data object containing the image data.
|
||||
/// - parameter scale: The scale factor to assume when interpreting the image data. Applying a scale factor of 1.0
|
||||
/// results in an image whose size matches the pixel-based dimensions of the image. Applying a
|
||||
/// different scale factor changes the size of the image as reported by the size property.
|
||||
///
|
||||
/// - returns: An initialized `UIImage` object, or `nil` if the method failed.
|
||||
public static func threadSafeImage(with data: Data, scale: CGFloat) -> UIImage? {
|
||||
lock.lock()
|
||||
let image = UIImage(data: data, scale: scale)
|
||||
lock.unlock()
|
||||
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
@available(*, deprecated, message: "Replaced by `UIImage.af.threadSafeImage(with:)`")
|
||||
public static func af_threadSafeImage(with data: Data) -> UIImage? {
|
||||
af.threadSafeImage(with: data)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `UIImage.af.threadSafeImage(with:scale:)`")
|
||||
public static func af_threadSafeImage(with data: Data, scale: CGFloat) -> UIImage? {
|
||||
af.threadSafeImage(with: data, scale: scale)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Inflation
|
||||
|
||||
extension AlamofireExtension where ExtendedType: UIImage {
|
||||
/// Returns whether the image is inflated.
|
||||
public var isInflated: Bool {
|
||||
get {
|
||||
if let isInflated = objc_getAssociatedObject(type, &AssociatedKeys.isInflated) as? Bool {
|
||||
return isInflated
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
nonmutating set {
|
||||
objc_setAssociatedObject(type, &AssociatedKeys.isInflated, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inflates the underlying compressed image data to be backed by an uncompressed bitmap representation.
|
||||
///
|
||||
/// Inflating compressed image formats (such as PNG or JPEG) can significantly improve drawing performance as it
|
||||
/// allows a bitmap representation to be constructed in the background rather than on the main thread.
|
||||
public func inflate() {
|
||||
guard !isInflated else { return }
|
||||
|
||||
isInflated = true
|
||||
_ = type.cgImage?.dataProvider?.data
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
@available(*, deprecated, message: "Replaced by `image.af.isInflated`")
|
||||
public var af_inflated: Bool {
|
||||
af.isInflated
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `image.af.inflate()`")
|
||||
public func af_inflate() {
|
||||
af.inflate()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Alpha
|
||||
|
||||
extension AlamofireExtension where ExtendedType: UIImage {
|
||||
/// Returns whether the image contains an alpha component.
|
||||
public var containsAlphaComponent: Bool {
|
||||
let alphaInfo = type.cgImage?.alphaInfo
|
||||
|
||||
return (
|
||||
alphaInfo == .first ||
|
||||
alphaInfo == .last ||
|
||||
alphaInfo == .premultipliedFirst ||
|
||||
alphaInfo == .premultipliedLast
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns whether the image is opaque.
|
||||
public var isOpaque: Bool { !containsAlphaComponent }
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
@available(*, deprecated, message: "Replaced by `image.af.containsAlphaComponent`")
|
||||
public var af_containsAlphaComponent: Bool { af.containsAlphaComponent }
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `image.af.isOpaque`")
|
||||
public var af_isOpaque: Bool { af.isOpaque }
|
||||
}
|
||||
|
||||
// MARK: - Scaling
|
||||
|
||||
extension AlamofireExtension where ExtendedType: UIImage {
|
||||
/// Returns a new version of the image scaled to the specified size.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - size: The size to use when scaling the new image.
|
||||
/// - scale: The scale to set for the new image. Defaults to `nil` which will maintain the current image scale.
|
||||
///
|
||||
/// - Returns: The new image object.
|
||||
public func imageScaled(to size: CGSize, scale: CGFloat? = nil) -> UIImage {
|
||||
assert(size.width > 0 && size.height > 0, "You cannot safely scale an image to a zero width or height")
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, isOpaque, scale ?? type.scale)
|
||||
type.draw(in: CGRect(origin: .zero, size: size))
|
||||
|
||||
let scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? type
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return scaledImage
|
||||
}
|
||||
|
||||
/// Returns a new version of the image scaled from the center while maintaining the aspect ratio to fit within
|
||||
/// a specified size.
|
||||
///
|
||||
/// The resulting image contains an alpha component used to pad the width or height with the necessary transparent
|
||||
/// pixels to fit the specified size. In high performance critical situations, this may not be the optimal approach.
|
||||
/// To maintain an opaque image, you could compute the `scaledSize` manually, then use the `af.imageScaledToSize`
|
||||
/// method in conjunction with a `.Center` content mode to achieve the same visual result.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - size: The size to use when scaling the new image.
|
||||
/// - scale: The scale to set for the new image. Defaults to `nil` which will maintain the current image scale.
|
||||
///
|
||||
/// - Returns: A new image object.
|
||||
public func imageAspectScaled(toFit size: CGSize, scale: CGFloat? = nil) -> UIImage {
|
||||
assert(size.width > 0 && size.height > 0, "You cannot safely scale an image to a zero width or height")
|
||||
|
||||
let imageAspectRatio = type.size.width / type.size.height
|
||||
let canvasAspectRatio = size.width / size.height
|
||||
|
||||
var resizeFactor: CGFloat
|
||||
|
||||
if imageAspectRatio > canvasAspectRatio {
|
||||
resizeFactor = size.width / type.size.width
|
||||
} else {
|
||||
resizeFactor = size.height / type.size.height
|
||||
}
|
||||
|
||||
let scaledSize = CGSize(width: type.size.width * resizeFactor, height: type.size.height * resizeFactor)
|
||||
let origin = CGPoint(x: (size.width - scaledSize.width) / 2.0, y: (size.height - scaledSize.height) / 2.0)
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, scale ?? type.scale)
|
||||
type.draw(in: CGRect(origin: origin, size: scaledSize))
|
||||
|
||||
let scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? type
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return scaledImage
|
||||
}
|
||||
|
||||
/// Returns a new version of the image scaled from the center while maintaining the aspect ratio to fill a
|
||||
/// specified size. Any pixels that fall outside the specified size are clipped.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - size: The size to use when scaling the new image.
|
||||
/// - scale: The scale to set for the new image. Defaults to `nil` which will maintain the current image scale.
|
||||
///
|
||||
/// - Returns: A new image object.
|
||||
public func imageAspectScaled(toFill size: CGSize, scale: CGFloat? = nil) -> UIImage {
|
||||
assert(size.width > 0 && size.height > 0, "You cannot safely scale an image to a zero width or height")
|
||||
|
||||
let imageAspectRatio = type.size.width / type.size.height
|
||||
let canvasAspectRatio = size.width / size.height
|
||||
|
||||
var resizeFactor: CGFloat
|
||||
|
||||
if imageAspectRatio > canvasAspectRatio {
|
||||
resizeFactor = size.height / type.size.height
|
||||
} else {
|
||||
resizeFactor = size.width / type.size.width
|
||||
}
|
||||
|
||||
let scaledSize = CGSize(width: type.size.width * resizeFactor, height: type.size.height * resizeFactor)
|
||||
let origin = CGPoint(x: (size.width - scaledSize.width) / 2.0, y: (size.height - scaledSize.height) / 2.0)
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, isOpaque, scale ?? type.scale)
|
||||
type.draw(in: CGRect(origin: origin, size: scaledSize))
|
||||
|
||||
let scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? type
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return scaledImage
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
@available(*, deprecated, message: "Replaced by `image.af.imageScale(to:scale:)`")
|
||||
public func af_imageScaled(to size: CGSize, scale: CGFloat? = nil) -> UIImage {
|
||||
af.imageScaled(to: size, scale: scale)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `image.af.imageAspectScale(toFit:scale:)`")
|
||||
public func af_imageAspectScaled(toFit size: CGSize, scale: CGFloat? = nil) -> UIImage {
|
||||
af.imageAspectScaled(toFit: size, scale: scale)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `image.af.imageAspectScale(toFill:scale:)`")
|
||||
public func af_imageAspectScaled(toFill size: CGSize, scale: CGFloat? = nil) -> UIImage {
|
||||
af.imageAspectScaled(toFill: size, scale: scale)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Rounded Corners
|
||||
|
||||
extension AlamofireExtension where ExtendedType: UIImage {
|
||||
/// Returns a new version of the image with the corners rounded to the specified radius.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - radius: The radius to use when rounding the new image.
|
||||
/// - divideRadiusByImageScale: Whether to divide the radius by the image scale. Set to `true` when the image has
|
||||
/// the same resolution for all screen scales such as @1x, @2x and @3x (i.e. single
|
||||
/// image from web server). Set to `false` for images loaded from an asset catalog
|
||||
/// with varying resolutions for each screen scale. `false` by default.
|
||||
///
|
||||
/// - Returns: A new image object.
|
||||
public func imageRounded(withCornerRadius radius: CGFloat, divideRadiusByImageScale: Bool = false) -> UIImage {
|
||||
let size = type.size
|
||||
let scale = type.scale
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, scale)
|
||||
|
||||
let scaledRadius = divideRadiusByImageScale ? radius / scale : radius
|
||||
|
||||
let clippingPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint.zero, size: size), cornerRadius: scaledRadius)
|
||||
clippingPath.addClip()
|
||||
|
||||
type.draw(in: CGRect(origin: CGPoint.zero, size: size))
|
||||
|
||||
let roundedImage = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return roundedImage
|
||||
}
|
||||
|
||||
/// Returns a new version of the image rounded into a circle.
|
||||
///
|
||||
/// - Returns: A new image object.
|
||||
public func imageRoundedIntoCircle() -> UIImage {
|
||||
let size = type.size
|
||||
let radius = min(size.width, size.height) / 2.0
|
||||
var squareImage: UIImage = type
|
||||
|
||||
if size.width != size.height {
|
||||
let squareDimension = min(size.width, size.height)
|
||||
let squareSize = CGSize(width: squareDimension, height: squareDimension)
|
||||
squareImage = imageAspectScaled(toFill: squareSize)
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(squareImage.size, false, type.scale)
|
||||
|
||||
let clippingPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint.zero, size: squareImage.size),
|
||||
cornerRadius: radius)
|
||||
|
||||
clippingPath.addClip()
|
||||
|
||||
squareImage.draw(in: CGRect(origin: CGPoint.zero, size: squareImage.size))
|
||||
|
||||
let roundedImage = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return roundedImage
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
@available(*, deprecated, message: "Replaced by `image.af.imageRounded(withCornerRadius:divideRadiusByImageScale:)`")
|
||||
public func af_imageRounded(withCornerRadius radius: CGFloat, divideRadiusByImageScale: Bool = false) -> UIImage {
|
||||
af.imageRounded(withCornerRadius: radius, divideRadiusByImageScale: divideRadiusByImageScale)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `image.af.imageRoundedIntoCircle()`")
|
||||
public func af_imageRoundedIntoCircle() -> UIImage {
|
||||
af.imageRoundedIntoCircle()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
|
||||
import CoreImage
|
||||
|
||||
// MARK: - Core Image Filters
|
||||
|
||||
extension AlamofireExtension where ExtendedType: UIImage {
|
||||
/// Returns a new version of the image using a CoreImage filter with the specified name and parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the CoreImage filter to use on the new image.
|
||||
/// - parameters: The parameters to apply to the CoreImage filter.
|
||||
///
|
||||
/// - Returns: A new image object, or `nil` if the filter failed for any reason.
|
||||
public func imageFiltered(withCoreImageFilter name: String, parameters: [String: Any]? = nil) -> UIImage? {
|
||||
var image: CoreImage.CIImage? = type.ciImage
|
||||
|
||||
if image == nil, let CGImage = type.cgImage {
|
||||
image = CoreImage.CIImage(cgImage: CGImage)
|
||||
}
|
||||
|
||||
guard let coreImage = image else { return nil }
|
||||
|
||||
let context = CIContext(options: [.priorityRequestLow: true])
|
||||
|
||||
var parameters: [String: Any] = parameters ?? [:]
|
||||
parameters[kCIInputImageKey] = coreImage
|
||||
|
||||
guard let filter = CIFilter(name: name, parameters: parameters) else { return nil }
|
||||
guard let outputImage = filter.outputImage else { return nil }
|
||||
|
||||
let cgImageRef = context.createCGImage(outputImage, from: outputImage.extent)
|
||||
|
||||
return UIImage(cgImage: cgImageRef!, scale: type.scale, orientation: type.imageOrientation)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
@available(*, deprecated, message: "Replaced by `image.af.imageFiltered(withCoreImageFilter:parameters:)`")
|
||||
public func af_imageFiltered(withCoreImageFilter name: String, parameters: [String: Any]? = nil) -> UIImage? {
|
||||
af.imageFiltered(withCoreImageFilter: name, parameters: parameters)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: -
|
||||
|
||||
private enum AssociatedKeys {
|
||||
static var isInflated = "UIImage.af.isInflated"
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
//
|
||||
// UIImageView+AlamofireImage.swift
|
||||
//
|
||||
// Copyright (c) 2015 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// 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 Alamofire
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
|
||||
import UIKit
|
||||
|
||||
public typealias AnimationOptions = UIView.AnimationOptions
|
||||
|
||||
extension UIImageView {
|
||||
/// Used to wrap all `UIView` animation transition options alongside a duration.
|
||||
public enum ImageTransition {
|
||||
case noTransition
|
||||
case crossDissolve(TimeInterval)
|
||||
case curlDown(TimeInterval)
|
||||
case curlUp(TimeInterval)
|
||||
case flipFromBottom(TimeInterval)
|
||||
case flipFromLeft(TimeInterval)
|
||||
case flipFromRight(TimeInterval)
|
||||
case flipFromTop(TimeInterval)
|
||||
case custom(duration: TimeInterval,
|
||||
animationOptions: AnimationOptions,
|
||||
animations: (UIImageView, Image) -> Void,
|
||||
completion: ((Bool) -> Void)?)
|
||||
|
||||
/// The duration of the image transition in seconds.
|
||||
public var duration: TimeInterval {
|
||||
switch self {
|
||||
case .noTransition:
|
||||
return 0.0
|
||||
case let .crossDissolve(duration):
|
||||
return duration
|
||||
case let .curlDown(duration):
|
||||
return duration
|
||||
case let .curlUp(duration):
|
||||
return duration
|
||||
case let .flipFromBottom(duration):
|
||||
return duration
|
||||
case let .flipFromLeft(duration):
|
||||
return duration
|
||||
case let .flipFromRight(duration):
|
||||
return duration
|
||||
case let .flipFromTop(duration):
|
||||
return duration
|
||||
case let .custom(duration, _, _, _):
|
||||
return duration
|
||||
}
|
||||
}
|
||||
|
||||
/// The animation options of the image transition.
|
||||
public var animationOptions: AnimationOptions {
|
||||
switch self {
|
||||
case .noTransition:
|
||||
return []
|
||||
case .crossDissolve:
|
||||
return .transitionCrossDissolve
|
||||
case .curlDown:
|
||||
return .transitionCurlDown
|
||||
case .curlUp:
|
||||
return .transitionCurlUp
|
||||
case .flipFromBottom:
|
||||
return .transitionFlipFromBottom
|
||||
case .flipFromLeft:
|
||||
return .transitionFlipFromLeft
|
||||
case .flipFromRight:
|
||||
return .transitionFlipFromRight
|
||||
case .flipFromTop:
|
||||
return .transitionFlipFromTop
|
||||
case let .custom(_, animationOptions, _, _):
|
||||
return animationOptions
|
||||
}
|
||||
}
|
||||
|
||||
/// The animation options of the image transition.
|
||||
public var animations: (UIImageView, Image) -> Void {
|
||||
switch self {
|
||||
case let .custom(_, _, animations, _):
|
||||
return animations
|
||||
default:
|
||||
return { $0.image = $1 }
|
||||
}
|
||||
}
|
||||
|
||||
/// The completion closure associated with the image transition.
|
||||
public var completion: ((Bool) -> Void)? {
|
||||
switch self {
|
||||
case let .custom(_, _, _, completion):
|
||||
return completion
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension UIImageView: AlamofireExtended {}
|
||||
extension AlamofireExtension where ExtendedType: UIImageView {
|
||||
// MARK: - Properties
|
||||
|
||||
/// The instance image downloader used to download all images. If this property is `nil`, the `UIImageView` will
|
||||
/// fallback on the `sharedImageDownloader` for all downloads. The most common use case for needing to use a custom
|
||||
/// instance image downloader is when images are behind different basic auth credentials.
|
||||
public var imageDownloader: ImageDownloader? {
|
||||
get {
|
||||
objc_getAssociatedObject(type, &AssociatedKeys.imageDownloader) as? ImageDownloader
|
||||
}
|
||||
nonmutating set(downloader) {
|
||||
objc_setAssociatedObject(type, &AssociatedKeys.imageDownloader, downloader, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
/// The shared image downloader used to download all images. By default, this is the default `ImageDownloader`
|
||||
/// instance backed with an `AutoPurgingImageCache` which automatically evicts images from the cache when the memory
|
||||
/// capacity is reached or memory warning notifications occur. The shared image downloader is only used if the
|
||||
/// `imageDownloader` is `nil`.
|
||||
public static var sharedImageDownloader: ImageDownloader {
|
||||
get {
|
||||
if let downloader = objc_getAssociatedObject(UIImageView.self, &AssociatedKeys.sharedImageDownloader) as? ImageDownloader {
|
||||
return downloader
|
||||
} else {
|
||||
return ImageDownloader.default
|
||||
}
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(UIImageView.self, &AssociatedKeys.sharedImageDownloader, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
var activeRequestReceipt: RequestReceipt? {
|
||||
get {
|
||||
objc_getAssociatedObject(type, &AssociatedKeys.activeRequestReceipt) as? RequestReceipt
|
||||
}
|
||||
nonmutating set {
|
||||
objc_setAssociatedObject(type, &AssociatedKeys.activeRequestReceipt, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Download
|
||||
|
||||
/// Asynchronously downloads an image from the specified URL, applies the specified image filter to the downloaded
|
||||
/// image and sets it once finished while executing the image transition.
|
||||
///
|
||||
/// If the image is cached locally, the image is set immediately. Otherwise the specified placeholder image will be
|
||||
/// set immediately, and then the remote image will be set once the image request is finished.
|
||||
///
|
||||
/// The `completion` closure is called after the image download and filtering are complete, but before the start of
|
||||
/// the image transition. Please note it is no longer the responsibility of the `completion` closure to set the
|
||||
/// image. It will be set automatically. If you require a second notification after the image transition completes,
|
||||
/// use a `.Custom` image transition with a `completion` closure. The `.Custom` `completion` closure is called when
|
||||
/// the image transition is finished.
|
||||
///
|
||||
/// - parameter url: The URL used for the image request.
|
||||
/// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults
|
||||
/// to `nil`.
|
||||
/// - parameter placeholderImage: The image to be set initially until the image request finished. If
|
||||
/// `nil`, the image view will not change its image until the image
|
||||
/// request finishes. Defaults to `nil`.
|
||||
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`.
|
||||
/// Defaults to `nil` which will fall back to the
|
||||
/// instance `imageResponseSerializer` set on the `ImageDownloader`.
|
||||
/// - parameter filter: The image filter applied to the image after the image request is
|
||||
/// finished. Defaults to `nil`.
|
||||
/// - parameter progress: The closure to be executed periodically during the lifecycle of the
|
||||
/// request. Defaults to `nil`.
|
||||
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the
|
||||
/// main queue.
|
||||
/// - parameter imageTransition: The image transition animation applied to the image when set.
|
||||
/// Defaults to `.None`.
|
||||
/// - parameter runImageTransitionIfCached: Whether to run the image transition if the image is cached. Defaults
|
||||
/// to `false`.
|
||||
/// - parameter completion: A closure to be executed when the image request finishes. The closure
|
||||
/// has no return value and takes three arguments: the original request,
|
||||
/// the response from the server and the result containing either the
|
||||
/// image or the error that occurred. If the image was returned from the
|
||||
/// image cache, the response will be `nil`. Defaults to `nil`.
|
||||
public func setImage(withURL url: URL,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
imageTransition: UIImageView.ImageTransition = .noTransition,
|
||||
runImageTransitionIfCached: Bool = false,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
setImage(withURLRequest: urlRequest(with: url),
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
imageTransition: imageTransition,
|
||||
runImageTransitionIfCached: runImageTransitionIfCached,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Asynchronously downloads an image from the specified URL Request, applies the specified image filter to the downloaded
|
||||
/// image and sets it once finished while executing the image transition.
|
||||
///
|
||||
/// If the image is cached locally, the image is set immediately. Otherwise the specified placeholder image will be
|
||||
/// set immediately, and then the remote image will be set once the image request is finished.
|
||||
///
|
||||
/// The `completion` closure is called after the image download and filtering are complete, but before the start of
|
||||
/// the image transition. Please note it is no longer the responsibility of the `completion` closure to set the
|
||||
/// image. It will be set automatically. If you require a second notification after the image transition completes,
|
||||
/// use a `.Custom` image transition with a `completion` closure. The `.Custom` `completion` closure is called when
|
||||
/// the image transition is finished.
|
||||
///
|
||||
/// - parameter urlRequest: The URL request.
|
||||
/// - parameter cacheKey: An optional key used to identify the image in the cache. Defaults
|
||||
/// to `nil`.
|
||||
/// - parameter placeholderImage: The image to be set initially until the image request finished. If
|
||||
/// `nil`, the image view will not change its image until the image
|
||||
/// request finishes. Defaults to `nil`.
|
||||
/// - parameter serializer: Image response serializer used to convert the image data to `UIImage`.
|
||||
/// Defaults to `nil` which will fall back to the
|
||||
/// instance `imageResponseSerializer` set on the `ImageDownloader`.
|
||||
/// - parameter filter: The image filter applied to the image after the image request is
|
||||
/// finished. Defaults to `nil`.
|
||||
/// - parameter progress: The closure to be executed periodically during the lifecycle of the
|
||||
/// request. Defaults to `nil`.
|
||||
/// - parameter progressQueue: The dispatch queue to call the progress closure on. Defaults to the
|
||||
/// main queue.
|
||||
/// - parameter imageTransition: The image transition animation applied to the image when set.
|
||||
/// Defaults to `.None`.
|
||||
/// - parameter runImageTransitionIfCached: Whether to run the image transition if the image is cached. Defaults
|
||||
/// to `false`.
|
||||
/// - parameter completion: A closure to be executed when the image request finishes. The closure
|
||||
/// has no return value and takes three arguments: the original request,
|
||||
/// the response from the server and the result containing either the
|
||||
/// image or the error that occurred. If the image was returned from the
|
||||
/// image cache, the response will be `nil`. Defaults to `nil`.
|
||||
public func setImage(withURLRequest urlRequest: URLRequestConvertible,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
imageTransition: UIImageView.ImageTransition = .noTransition,
|
||||
runImageTransitionIfCached: Bool = false,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
guard !isURLRequestURLEqualToActiveRequestURL(urlRequest) else {
|
||||
let response = AFIDataResponse<UIImage>(request: nil,
|
||||
response: nil,
|
||||
data: nil,
|
||||
metrics: nil,
|
||||
serializationDuration: 0.0,
|
||||
result: .failure(AFIError.requestCancelled))
|
||||
|
||||
completion?(response)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cancelImageRequest()
|
||||
|
||||
let imageDownloader = self.imageDownloader ?? UIImageView.af.sharedImageDownloader
|
||||
let imageCache = imageDownloader.imageCache
|
||||
|
||||
// Use the image from the image cache if it exists
|
||||
if let request = urlRequest.urlRequest {
|
||||
let cachedImage: Image?
|
||||
|
||||
if let cacheKey = cacheKey {
|
||||
cachedImage = imageCache?.image(withIdentifier: cacheKey)
|
||||
} else {
|
||||
cachedImage = imageCache?.image(for: request, withIdentifier: filter?.identifier)
|
||||
}
|
||||
|
||||
if let image = cachedImage {
|
||||
let response = AFIDataResponse<UIImage>(request: request,
|
||||
response: nil,
|
||||
data: nil,
|
||||
metrics: nil,
|
||||
serializationDuration: 0.0,
|
||||
result: .success(image))
|
||||
|
||||
if runImageTransitionIfCached {
|
||||
// It's important to display the placeholder image again otherwise you have some odd disparity
|
||||
// between the request loading from the cache and those that download. It's important to keep
|
||||
// the same behavior between both, otherwise the user can actually see the difference.
|
||||
if let placeholderImage = placeholderImage { type.image = placeholderImage }
|
||||
|
||||
// Need to let the runloop cycle for the placeholder image to take affect
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) {
|
||||
// Added this additional check to ensure another request didn't get in during the delay
|
||||
guard self.activeRequestReceipt == nil else { return }
|
||||
|
||||
self.run(imageTransition, with: image)
|
||||
completion?(response)
|
||||
}
|
||||
} else {
|
||||
type.image = image
|
||||
completion?(response)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set the placeholder since we're going to have to download
|
||||
if let placeholderImage = placeholderImage { type.image = placeholderImage }
|
||||
|
||||
// Generate a unique download id to check whether the active request has changed while downloading
|
||||
let downloadID = UUID().uuidString
|
||||
|
||||
// Weakify the image view to allow it to go out-of-memory while download is running if deallocated
|
||||
weak var imageView = type
|
||||
|
||||
// Download the image, then run the image transition or completion handler
|
||||
let requestReceipt = imageDownloader.download(urlRequest,
|
||||
cacheKey: cacheKey,
|
||||
receiptID: downloadID,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
completion: { response in
|
||||
guard
|
||||
let strongSelf = imageView?.af,
|
||||
strongSelf.isURLRequestURLEqualToActiveRequestURL(response.request) &&
|
||||
strongSelf.activeRequestReceipt?.receiptID == downloadID
|
||||
else {
|
||||
completion?(response)
|
||||
return
|
||||
}
|
||||
|
||||
if case let .success(image) = response.result {
|
||||
strongSelf.run(imageTransition, with: image)
|
||||
}
|
||||
|
||||
strongSelf.activeRequestReceipt = nil
|
||||
|
||||
completion?(response)
|
||||
})
|
||||
|
||||
activeRequestReceipt = requestReceipt
|
||||
}
|
||||
|
||||
// MARK: - Image Download Cancellation
|
||||
|
||||
/// Cancels the active download request, if one exists.
|
||||
public func cancelImageRequest() {
|
||||
guard let activeRequestReceipt = activeRequestReceipt else { return }
|
||||
|
||||
let imageDownloader = self.imageDownloader ?? UIImageView.af.sharedImageDownloader
|
||||
imageDownloader.cancelRequest(with: activeRequestReceipt)
|
||||
|
||||
self.activeRequestReceipt = nil
|
||||
}
|
||||
|
||||
// MARK: - Image Transition
|
||||
|
||||
/// Runs the image transition on the image view with the specified image.
|
||||
///
|
||||
/// - parameter imageTransition: The image transition to ran on the image view.
|
||||
/// - parameter image: The image to use for the image transition.
|
||||
public func run(_ imageTransition: UIImageView.ImageTransition, with image: Image) {
|
||||
let imageView = type
|
||||
|
||||
UIView.transition(with: type,
|
||||
duration: imageTransition.duration,
|
||||
options: imageTransition.animationOptions,
|
||||
animations: { imageTransition.animations(imageView, image) },
|
||||
completion: imageTransition.completion)
|
||||
}
|
||||
|
||||
// MARK: - Private - URL Request Helper Methods
|
||||
|
||||
private func urlRequest(with url: URL) -> URLRequest {
|
||||
var urlRequest = URLRequest(url: url)
|
||||
|
||||
for mimeType in ImageResponseSerializer.acceptableImageContentTypes.sorted() {
|
||||
urlRequest.addValue(mimeType, forHTTPHeaderField: "Accept")
|
||||
}
|
||||
|
||||
return urlRequest
|
||||
}
|
||||
|
||||
private func isURLRequestURLEqualToActiveRequestURL(_ urlRequest: URLRequestConvertible?) -> Bool {
|
||||
if
|
||||
let currentRequestURL = activeRequestReceipt?.request.task?.originalRequest?.url,
|
||||
let requestURL = urlRequest?.urlRequest?.url,
|
||||
currentRequestURL == requestURL {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Deprecated
|
||||
|
||||
extension UIImageView {
|
||||
@available(*, deprecated, message: "Replaced by `imageView.af.imageDownloader`")
|
||||
public var af_imageDownloader: ImageDownloader? {
|
||||
get { af.imageDownloader }
|
||||
set { af.imageDownloader = newValue }
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `imageView.af.sharedImageDownloader`")
|
||||
public class var af_sharedImageDownloader: ImageDownloader {
|
||||
get { af.sharedImageDownloader }
|
||||
set { af.sharedImageDownloader = newValue }
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `imageView.af.setImage(withURL: ...)`")
|
||||
public func af_setImage(withURL url: URL,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
imageTransition: ImageTransition = .noTransition,
|
||||
runImageTransitionIfCached: Bool = false,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
af.setImage(withURL: url,
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
imageTransition: imageTransition,
|
||||
runImageTransitionIfCached: runImageTransitionIfCached,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `imageView.af.setImage(withURLRequest: ...)`")
|
||||
public func af_setImage(withURLRequest urlRequest: URLRequestConvertible,
|
||||
cacheKey: String? = nil,
|
||||
placeholderImage: UIImage? = nil,
|
||||
serializer: ImageResponseSerializer? = nil,
|
||||
filter: ImageFilter? = nil,
|
||||
progress: ImageDownloader.ProgressHandler? = nil,
|
||||
progressQueue: DispatchQueue = DispatchQueue.main,
|
||||
imageTransition: ImageTransition = .noTransition,
|
||||
runImageTransitionIfCached: Bool = false,
|
||||
completion: ((AFIDataResponse<UIImage>) -> Void)? = nil) {
|
||||
af.setImage(withURLRequest: urlRequest,
|
||||
cacheKey: cacheKey,
|
||||
placeholderImage: placeholderImage,
|
||||
serializer: serializer,
|
||||
filter: filter,
|
||||
progress: progress,
|
||||
progressQueue: progressQueue,
|
||||
imageTransition: imageTransition,
|
||||
runImageTransitionIfCached: runImageTransitionIfCached,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `imageView.af.cancelImageRequest()`")
|
||||
public func af_cancelImageRequest() {
|
||||
af.cancelImageRequest()
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Replaced by `imageView.af.run(_:with:)`")
|
||||
public func run(_ imageTransition: ImageTransition, with image: Image) {
|
||||
af.run(imageTransition, with: image)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private enum AssociatedKeys {
|
||||
static var imageDownloader = "UIImageView.af.imageDownloader"
|
||||
static var sharedImageDownloader = "UIImageView.af.sharedImageDownloader"
|
||||
static var activeRequestReceipt = "UIImageView.af.activeRequestReceipt"
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user