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
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2015-2021 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.
+576
View File
@@ -0,0 +1,576 @@
# AlamofireImage
[![Build Status](https://travis-ci.org/Alamofire/AlamofireImage.svg?branch=master)](https://travis-ci.org/Alamofire/AlamofireImage)
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/AlamofireImage.svg)](https://img.shields.io/cocoapods/v/AlamofireImage.svg)
[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Platform](https://img.shields.io/cocoapods/p/AlamofireImage.svg?style=flat)](http://cocoadocs.org/docsets/AlamofireImage)
[![Twitter](https://img.shields.io/badge/twitter-@AlamofireSF-blue.svg?style=flat)](http://twitter.com/AlamofireSF)
[![Gitter](https://badges.gitter.im/Alamofire/Alamofire.svg)](https://gitter.im/Alamofire/Alamofire?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
AlamofireImage is an image component library for Alamofire.
## Features
- [x] Image Response Serializers
- [x] UIImage Extensions for Inflation / Scaling / Rounding / CoreImage
- [x] Single and Multi-Pass Image Filters
- [x] Auto-Purging In-Memory Image Cache
- [x] Prioritized Queue Order Image Downloading
- [x] Authentication with URLCredential
- [x] UIImageView Async Remote Downloads with Placeholders
- [x] UIImageView Filters and Transitions
- [x] Comprehensive Test Coverage
- [x] [Complete Documentation](https://alamofire.github.io/AlamofireImage/)
## Requirements
- iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
- Xcode 11+
- Swift 5.1+
## Migration Guides
- [AlamofireImage 2.0 Migration Guide](https://github.com/Alamofire/AlamofireImage/blob/master/Documentation/AlamofireImage%202.0%20Migration%20Guide.md)
- [AlamofireImage 3.0 Migration Guide](https://github.com/Alamofire/AlamofireImage/blob/master/Documentation/AlamofireImage%203.0%20Migration%20Guide.md)
- [AlamofireImage 4.0 Migration Guide](https://github.com/Alamofire/AlamofireImage/blob/master/Documentation/AlamofireImage%204.0%20Migration%20Guide.md)
## Dependencies
- [Alamofire 5.1+](https://github.com/Alamofire/Alamofire)
## Communication
- If you need to **find or understand an API**, check [our documentation](https://alamofire.github.io/AlamofireImage/).
- If you need **help with an AlamofireImage feature**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire).
- If you'd like to **discuss AlamofireImage best practices**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire).
- If you'd like to **discuss a feature request**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire).
- If you **found a bug**, open an issue and follow the guide. The more detail the better!
- If you **want to contribute**, submit a pull request.
## Installation
### CocoaPods
[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate AlamofireImage into your Xcode project using CocoaPods, specify it in your `Podfile`:
```ruby
pod 'AlamofireImage', '~> 4.1'
```
### Carthage
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate AlamofireImage into your Xcode project using Carthage, specify it in your `Cartfile`:
```ogdl
github "Alamofire/AlamofireImage" ~> 4.1
```
### Swift Package Manager
The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but AlamofireImage does support its use on supported platforms.
Once you have your Swift package set up, adding AlamofireImage as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
```swift
dependencies: [
.package(url: "https://github.com/Alamofire/AlamofireImage.git", .upToNextMajor(from: "4.2.0"))
]
```
### Manually
If you prefer not to use either of the aforementioned dependency managers, you can integrate AlamofireImage into your project manually.
#### Embedded Framework
- Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository:
```bash
$ git init
```
- Add AlamofireImage as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command:
```bash
$ git submodule add https://github.com/Alamofire/AlamofireImage.git
```
- Open the new `AlamofireImage` folder, and drag the `AlamofireImage.xcodeproj` into the Project Navigator of your application's Xcode project.
> It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.
- Select the `AlamofireImage.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target.
- Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar.
- In the tab bar at the top of that window, open the "General" panel.
- Click on the `+` button under the "Embedded Binaries" section.
- You will see two different `AlamofireImage.xcodeproj` folders each with two different versions of the `AlamofireImage.framework` nested inside a `Products` folder.
> It does not matter which `Products` folder you choose from, but it does matter whether you choose the top or bottom `AlamofireImage.framework`.
- Select the top `AlamofireImage.framework` for iOS and the bottom one for OS X.
> You can verify which one you selected by inspecting the build log for your project. The build target for `AlamofireImage` will be listed as either `AlamofireImage iOS`, `AlamofireImage macOS`, `AlamofireImage tvOS` or `AlamofireImage watchOS`.
- And that's it!
> The `AlamofireImage.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.
---
## Usage
### Image Response Serializers
```swift
import Alamofire
import AlamofireImage
Alamofire.request("https://httpbin.org/image/png").responseImage { response in
debugPrint(response)
print(response.request)
print(response.response)
debugPrint(response.result)
if case .success(let image) = response.result {
print("image downloaded: \(image)")
}
}
```
The AlamofireImage response image serializers support a wide range of image types including:
- `image/png`
- `image/jpeg`
- `image/tiff`
- `image/gif`
- `image/ico`
- `image/x-icon`
- `image/bmp`
- `image/x-bmp`
- `image/x-xbitmap`
- `image/x-ms-bmp`
- `image/x-win-bitmap`
- `image/heic`
- `application/octet-stream` (added for iOS 13 support)
> If the image you are attempting to download is an invalid MIME type not in the list, you can add custom acceptable content types using the `addAcceptableImageContentTypes` extension on the `DataRequest` type.
### UIImage Extensions
There are several `UIImage` extensions designed to make the common image manipulation operations as simple as possible.
#### Inflation
```swift
let url = Bundle.main.url(forResource: "unicorn", withExtension: "png")!
let data = try! Data(contentsOf: url)
let image = UIImage(data: data, scale: UIScreen.main.scale)!
image.af.inflate()
```
> Inflating compressed image formats (such as PNG or JPEG) in a background queue can significantly improve drawing performance on the main thread.
#### Scaling
```swift
let image = UIImage(named: "unicorn")!
let size = CGSize(width: 100.0, height: 100.0)
// Scale image to size disregarding aspect ratio
let scaledImage = image.af.imageScaled(to: size)
// Scale image to fit within specified size while maintaining aspect ratio
let aspectScaledToFitImage = image.af.imageAspectScaled(toFit: size)
// Scale image to fill specified size while maintaining aspect ratio
let aspectScaledToFillImage = image.af.imageAspectScaled(toFill: size)
```
#### Rounded Corners
```swift
let image = UIImage(named: "unicorn")!
let radius: CGFloat = 20.0
let roundedImage = image.af.imageRounded(withCornerRadius: radius)
let circularImage = image.af.imageRoundedIntoCircle()
```
#### Core Image Filters
```swift
let image = UIImage(named: "unicorn")!
let sepiaImage = image.af.imageFiltered(withCoreImageFilter: "CISepiaTone")
let blurredImage = image.af.imageFiltered(
withCoreImageFilter: "CIGaussianBlur",
parameters: ["inputRadius": 25]
)
```
### Image Filters
The `ImageFilter` protocol was designed to make it easy to apply a filter operation and cache the result after an image finished downloading. It defines two properties to facilitate this functionality.
```swift
public protocol ImageFilter {
var filter: Image -> Image { get }
var identifier: String { get }
}
```
The `filter` closure contains the operation used to create a modified version of the specified image. The `identifier` property is a string used to uniquely identify the filter operation. This is useful when adding filtered versions of an image to a cache. All identifier properties inside AlamofireImage are implemented using protocol extensions.
#### Single Pass
The single pass image filters only perform a single operation on the specified image.
```swift
let image = UIImage(named: "unicorn")!
let imageFilter = RoundedCornersFilter(radius: 10.0)
let roundedImage = imageFilter.filter(image)
```
The current list of single pass image filters includes:
- `ScaledToSizeFilter` - Scales an image to a specified size.
- `AspectScaledToFitSizeFilter` - Scales an image from the center while maintaining the aspect ratio to fit within a specified size.
- `AspectScaledToFillSizeFilter` - 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.
- `RoundedCornersFilter` - Rounds the corners of an image to the specified radius.
- `CircleFilter` - Rounds the corners of an image into a circle.
- `BlurFilter` - Blurs an image using a `CIGaussianBlur` filter with the specified blur radius.
> Each image filter is built ontop of the `UIImage` extensions.
#### Multi-Pass
The multi-pass image filters perform multiple operations on the specified image.
```swift
let image = UIImage(named: "avatar")!
let size = CGSize(width: 100.0, height: 100.0)
let imageFilter = AspectScaledToFillSizeCircleFilter(size: size)
let avatarImage = imageFilter.filter(image)
```
The current list of multi-pass image filters includes:
- `ScaledToSizeWithRoundedCornersFilter` - Scales an image to a specified size, then rounds the corners to the specified radius.
- `AspectScaledToFillSizeWithRoundedCornersFilter` - 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.
- `ScaledToSizeCircleFilter` - Scales an image to a specified size, then rounds the corners into a circle.
- `AspectScaledToFillSizeCircleFilter` - Scales an image from the center while maintaining the aspect ratio to fit within a specified size, then rounds the corners into a circle.
### Image Cache
Image caching can become complicated when it comes to network images. `URLCache` is quite powerful and does a great job reasoning through the various cache policies and `Cache-Control` headers. However, it is not equipped to handle caching multiple modified versions of those images.
For example, let's say you need to download an album of images. Your app needs to display both the thumbnail version as well as the full size version at various times. Due to performance issues, you want to scale down the thumbnails to a reasonable size before rendering them on-screen. You also need to apply a global CoreImage filter to the full size images when displayed. While `URLCache` can easily handle storing the original downloaded image, it cannot store these different variants. What you really need is another caching layer designed to handle these different variants.
```swift
let imageCache = AutoPurgingImageCache(
memoryCapacity: 100_000_000,
preferredMemoryUsageAfterPurge: 60_000_000
)
```
The `AutoPurgingImageCache` in AlamofireImage fills the role of that additional caching layer. It is 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.
#### Add / Remove / Fetch Images
Interacting with the `ImageCache` protocol APIs is very straightforward.
```swift
let imageCache = AutoPurgingImageCache()
let avatarImage = UIImage(data: data)!
// Add
imageCache.add(avatarImage, withIdentifier: "avatar")
// Fetch
let cachedAvatar = imageCache.image(withIdentifier: "avatar")
// Remove
imageCache.removeImage(withIdentifier: "avatar")
```
#### URL Requests
The `ImageRequestCache` protocol extends the `ImageCache` protocol by adding support for `URLRequest` caching. This allows a `URLRequest` and an additional identifier to generate the unique identifier for the image in the cache.
```swift
let imageCache = AutoPurgingImageCache()
let urlRequest = URLRequest(url: URL(string: "https://httpbin.org/image/png")!)
let avatarImage = UIImage(named: "avatar")!.af.imageRoundedIntoCircle()
// Add
imageCache.add(avatarImage, for: urlRequest, withIdentifier: "circle")
// Fetch
let cachedAvatarImage = imageCache.image(for: urlRequest, withIdentifier: "circle")
// Remove
imageCache.removeImage(for: urlRequest, withIdentifier: "circle")
```
#### Auto-Purging
Each time an image is fetched from the cache, the cache internally updates the last access date for that image.
```swift
let avatar = imageCache.image(withIdentifier: "avatar")
let circularAvatar = imageCache.image(for: urlRequest, withIdentifier: "circle")
```
By updating the last access date for each image, the image cache can make more informed decisions about which images to purge when the memory capacity is reached. The `AutoPurgingImageCache` automatically evicts images from the cache in order from oldest last access date to newest until the memory capacity drops below the `preferredMemoryCapacityAfterPurge`.
> It is important to set reasonable default values for the `memoryCapacity` and `preferredMemoryCapacityAfterPurge` when you are initializing your image cache. By default, the `memoryCapacity` equals 100 MB and the `preferredMemoryCapacityAfterPurge` equals 60 MB.
#### Memory Warnings
The `AutoPurgingImageCache` also listens for memory warnings from your application and will purge all images from the cache if a memory warning is observed.
### Image Downloader
The `ImageDownloader` class is responsible for downloading images in parallel on a prioritized queue. It uses an internal Alamofire `SessionManager` instance to handle all the downloading and response image serialization. By default, the initialization of an `ImageDownloader` uses a default `URLSessionConfiguration` with the most common parameter values.
```swift
let imageDownloader = ImageDownloader(
configuration: ImageDownloader.defaultURLSessionConfiguration(),
downloadPrioritization: .fifo,
maximumActiveDownloads: 4,
imageCache: AutoPurgingImageCache()
)
```
> If you need to customize the `URLSessionConfiguration` type or parameters, then simply provide your own rather than using the default.
#### Downloading an Image
```swift
let downloader = ImageDownloader()
let urlRequest = URLRequest(url: URL(string: "https://httpbin.org/image/jpeg")!)
downloader.download(urlRequest) { response in
print(response.request)
print(response.response)
debugPrint(response.result)
if case .success(let image) = response.result {
print(image)
}
}
```
> Make sure to keep a strong reference to the `ImageDownloader` instance, otherwise the `completion` closure will not be called because the `downloader` reference will go out of scope before the `completion` closure can be called.
#### Applying an ImageFilter
```swift
let downloader = ImageDownloader()
let urlRequest = URLRequest(url: URL(string: "https://httpbin.org/image/jpeg")!)
let filter = AspectScaledToFillSizeCircleFilter(size: CGSize(width: 100.0, height: 100.0))
downloader.download(urlRequest, filter: filter) { response in
print(response.request)
print(response.response)
debugPrint(response.result)
if case .success(let image) = response.result {
print(image)
}
}
```
#### Authentication
If your images are behind HTTP Basic Auth, you can append the `user:password:` or the `credential` to the `ImageDownloader` instance. The credentials will be applied to all future download requests.
```swift
let downloader = ImageDownloader()
downloader.addAuthentication(user: "username", password: "password")
```
#### Download Prioritization
The `ImageDownloader` maintains an internal queue of pending download requests. Depending on your situation, you may want incoming downloads to be inserted at the front or the back of the queue. The `DownloadPrioritization` enumeration allows you to specify which behavior you would prefer.
```swift
public enum DownloadPrioritization {
case fifo, lifo
}
```
> The `ImageDownloader` is initialized with a `.fifo` queue by default.
#### Image Caching
The `ImageDownloader` uses a combination of an `URLCache` and `AutoPurgingImageCache` to create a very robust, high performance image caching system.
##### URLCache
The `URLCache` is used to cache all the original image content downloaded from the server. By default, it is initialized with a memory capacity of 20 MB and a disk capacity of 150 MB. This allows up to 150 MB of original image data to be stored on disk at any given time. While these defaults have been carefully set, it is very important to consider your application's needs and performance requirements and whether these values are right for you.
> If you wish to disable this caching layer, create a custom `URLSessionConfiguration` with the `urlCache` property set to `nil` and use that configuration when initializing the `ImageDownloader`.
##### Image Cache
The `ImageCache` is used to cache all the potentially filtered image content after it has been downloaded from the server. This allows multiple variants of the same image to also be cached, rather than having to re-apply the image filters to the original image each time it is required. By default, an `AutoPurgingImageCache` is initialized with a memory capacity of 100 MB and a preferred memory usage after purge limit of 60 MB. This allows up to 100 MB of most recently accessed filtered image content to be stored in-memory at a given time.
##### Setting Ideal Capacity Limits
Determining the ideal the in-memory and on-disk capacity limits of the `URLCache` and `AutoPurgingImageCache` requires a bit of forethought. You must carefully consider your application's needs, and tailor the limits accordingly. By default, the combination of caches offers the following storage capacities:
- 150 MB of on-disk storage (original image only)
- 20 MB of in-memory original image data storage (original image only)
- 100 MB of in-memory storage of filtered image content (filtered image if using filters, otherwise original image)
- 60 MB preferred memory capacity after purge of filtered image content
> If you do not use image filters, it is advised to set the memory capacity of the `URLCache` to zero. Otherwise, you will be storing the original image data in both the URLCache's in-memory store as well as the AlamofireImage in-memory store.
#### Duplicate Downloads
Sometimes application logic can end up attempting to download an image more than once before the initial download request is complete. Most often, this results in the image being downloaded more than once. AlamofireImage handles this case elegantly by merging the duplicate downloads. The image will only be downloaded once, yet both completion handlers will be called.
##### Image Filter Reuse
In addition to merging duplicate downloads, AlamofireImage can also merge duplicate image filters. If two image filters with the same identifier are attached to the same download, the image filter is only executed once and both completion handlers are called with the same resulting image. This can save large amounts of time and resources for computationally expensive filters such as ones leveraging CoreImage.
##### Request Receipts
Sometimes it is necessary to cancel an image download for various reasons. AlamofireImage can intelligently handle cancellation logic in the `ImageDownloader` by leveraging the `RequestReceipt` type along with the `cancelRequestForRequestReceipt` method. Each download request vends a `RequestReceipt` which can be later used to cancel the request.
By cancelling the request through the `ImageDownloader` using the `RequestReceipt`, AlamofireImage is able to determine how to best handle the cancellation. The cancelled download will always receive a cancellation error, while duplicate downloads are allowed to complete. If the download is already active, it is allowed to complete even though the completion handler will be called with a cancellation error. This greatly improves performance of table and collection views displaying large amounts of images.
> It is NOT recommended to directly call `cancel` on the `request` in the `RequestReceipt`. Doing so can lead to issues such as duplicate downloads never being allowed to complete.
### UIImageView Extension
The [UIImage Extensions](#uiimage-extensions), [Image Filters](#image-filters), [Image Cache](#image-cache) and [Image Downloader](#image-downloader) were all designed to be flexible and standalone, yet also to provide the foundation of the `UIImageView` extension. Due to the powerful support of these classes, protocols and extensions, the `UIImageView` APIs are concise, easy to use and contain a large amount of functionality.
#### Setting Image with URL
Setting the image with a URL will asynchronously download the image and set it once the request is finished.
```swift
let imageView = UIImageView(frame: frame)
let url = URL(string: "https://httpbin.org/image/png")!
imageView.af.setImage(withURL: url)
```
> If the image is cached locally, the image is set immediately.
#### Placeholder Images
By specifying a placeholder image, the image view uses the placeholder image until the remote image is downloaded.
```swift
let imageView = UIImageView(frame: frame)
let url = URL(string: "https://httpbin.org/image/png")!
let placeholderImage = UIImage(named: "placeholder")!
imageView.af.setImage(withURL: url, placeholderImage: placeholderImage)
```
> If the remote image is cached locally, the placeholder image is never set.
#### Image Filters
If an image filter is specified, it is applied asynchronously after the remote image is downloaded. Once the filter execution is complete, the resulting image is set on the image view.
```swift
let imageView = UIImageView(frame: frame)
let url = URL(string: "https://httpbin.org/image/png")!
let placeholderImage = UIImage(named: "placeholder")!
let filter = AspectScaledToFillSizeWithRoundedCornersFilter(
size: imageView.frame.size,
radius: 20.0
)
imageView.af.setImage(
withURL: url,
placeholderImage: placeholderImage,
filter: filter
)
```
> If the remote image with the applied filter is cached locally, the image is set immediately.
#### Image Transitions
By default, there is no image transition animation when setting the image on the image view. If you wish to add a cross dissolve or flip-from-bottom animation, then specify an `ImageTransition` with the preferred duration.
```swift
let imageView = UIImageView(frame: frame)
let url = URL(string: "https://httpbin.org/image/png")!
let placeholderImage = UIImage(named: "placeholder")!
let filter = AspectScaledToFillSizeWithRoundedCornersFilter(
size: imageView.frame.size,
radius: 20.0
)
imageView.af.setImage(
withURL: url,
placeholderImage: placeholderImage,
filter: filter,
imageTransition: .crossDissolve(0.2)
)
```
> If the remote image is cached locally, the image transition is ignored.
#### Image Downloader
The `UIImageView` extension is powered by the default `ImageDownloader` instance. To customize cache capacities, download priorities, request cache policies, timeout durations, etc., please refer to the [Image Downloader](#image-downloader) documentation.
##### Authentication
If an image requires and authentication credential from the `UIImageView` extension, it can be provided as follows:
```swift
ImageDownloader.default.addAuthentication(user: "user", password: "password")
```
---
## Credits
Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). You can follow them on Twitter at [@AlamofireSF](https://twitter.com/AlamofireSF) for project updates and releases.
### Security Disclosure
If you believe you have identified a security vulnerability with AlamofireImage, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker.
## Donations
The [ASF](https://github.com/Alamofire/Foundation#members) is looking to raise money to officially stay registered as a federal non-profit organization.
Registering will allow us members to gain some legal protections and also allow us to put donations to use, tax free.
Donating to the ASF will enable us to:
- Pay our yearly legal fees to keep the non-profit in good status
- Pay for our mail servers to help us stay on top of all questions and security issues
- Potentially fund test servers to make it easier for us to test the edge cases
- Potentially fund developers to work on one of our projects full-time
The community adoption of the ASF libraries has been amazing.
We are greatly humbled by your enthusiasm around the projects, and want to continue to do everything we can to move the needle forward.
With your continued support, the ASF will be able to improve its reach and also provide better legal safety for the core members.
If you use any of our libraries for work, see if your employers would be interested in donating.
Any amount you can donate today to help us reach our goal would be greatly appreciated.
[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=W34WPEE74APJQ)
## License
AlamofireImage is released under the MIT license. [See LICENSE](https://github.com/Alamofire/AlamofireImage/blob/master/LICENSE) for details.
+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