Project Setup

This commit is contained in:
manaknightdev
2023-03-13 23:20:27 +05:30
commit d4da2b5e02
178 changed files with 29139 additions and 0 deletions
+72
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+578
View File
@@ -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
View File
@@ -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