UIButton+AlamofireImage.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. // UIButton+AlamofireImage.swift
  2. //
  3. // Copyright (c) 2015-2016 Alamofire Software Foundation (http://alamofire.org/)
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. import Alamofire
  23. import Foundation
  24. import UIKit
  25. extension UIButton {
  26. // MARK: - Private - AssociatedKeys
  27. private struct AssociatedKeys {
  28. static var ImageDownloaderKey = "af_UIButton.ImageDownloader"
  29. static var SharedImageDownloaderKey = "af_UIButton.SharedImageDownloader"
  30. static var ImageReceiptsKey = "af_UIButton.ImageReceipts"
  31. static var BackgroundImageReceiptsKey = "af_UIButton.BackgroundImageReceipts"
  32. }
  33. // MARK: - Properties
  34. /// The instance image downloader used to download all images. If this property is `nil`, the `UIButton` will
  35. /// fallback on the `af_sharedImageDownloader` for all downloads. The most common use case for needing to use a
  36. /// custom instance image downloader is when images are behind different basic auth credentials.
  37. public var af_imageDownloader: ImageDownloader? {
  38. get {
  39. return objc_getAssociatedObject(self, &AssociatedKeys.ImageDownloaderKey) as? ImageDownloader
  40. }
  41. set {
  42. objc_setAssociatedObject(self, &AssociatedKeys.ImageDownloaderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  43. }
  44. }
  45. /// The shared image downloader used to download all images. By default, this is the default `ImageDownloader`
  46. /// instance backed with an `AutoPurgingImageCache` which automatically evicts images from the cache when the memory
  47. /// capacity is reached or memory warning notifications occur. The shared image downloader is only used if the
  48. /// `af_imageDownloader` is `nil`.
  49. public class var af_sharedImageDownloader: ImageDownloader {
  50. get {
  51. guard let
  52. downloader = objc_getAssociatedObject(self, &AssociatedKeys.SharedImageDownloaderKey) as? ImageDownloader
  53. else {
  54. return ImageDownloader.defaultInstance
  55. }
  56. return downloader
  57. }
  58. set {
  59. objc_setAssociatedObject(self, &AssociatedKeys.SharedImageDownloaderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  60. }
  61. }
  62. private var imageRequestReceipts: [UInt: RequestReceipt] {
  63. get {
  64. guard let
  65. receipts = objc_getAssociatedObject(self, &AssociatedKeys.ImageReceiptsKey) as? [UInt: RequestReceipt]
  66. else {
  67. return [:]
  68. }
  69. return receipts
  70. }
  71. set {
  72. objc_setAssociatedObject(self, &AssociatedKeys.ImageReceiptsKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  73. }
  74. }
  75. private var backgroundImageRequestReceipts: [UInt: RequestReceipt] {
  76. get {
  77. guard let
  78. receipts = objc_getAssociatedObject(self, &AssociatedKeys.BackgroundImageReceiptsKey) as? [UInt: RequestReceipt]
  79. else {
  80. return [:]
  81. }
  82. return receipts
  83. }
  84. set {
  85. objc_setAssociatedObject(self, &AssociatedKeys.BackgroundImageReceiptsKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  86. }
  87. }
  88. // MARK: - Image Downloads
  89. /**
  90. Asynchronously downloads an image from the specified URL and sets it once the request is finished.
  91. If the image is cached locally, the image is set immediately. Otherwise the specified placehoder image will be
  92. set immediately, and then the remote image will be set once the image request is finished.
  93. - parameter URL: The URL used for your image request.
  94. - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the
  95. image will not change its image until the image request finishes.
  96. Defaults to `nil`.
  97. - parameter completion: A closure to be executed when the image request finishes. The closure
  98. has no return value and takes three arguments: the original request,
  99. the response from the server and the result containing either the
  100. image or the error that occurred. If the image was returned from the
  101. image cache, the response will be `nil`. Defaults to `nil`.
  102. */
  103. public func af_setImageForState(
  104. state: UIControlState,
  105. URL: NSURL,
  106. placeHolderImage: UIImage? = nil,
  107. completion: (Response<UIImage, NSError> -> Void)? = nil)
  108. {
  109. af_setImageForState(state,
  110. URLRequest: URLRequestWithURL(URL),
  111. placeholderImage: placeHolderImage,
  112. completion: completion)
  113. }
  114. /**
  115. Asynchronously downloads an image from the specified URL request and sets it once the request is finished.
  116. If the image is cached locally, the image is set immediately. Otherwise the specified placehoder image will be
  117. set immediately, and then the remote image will be set once the image request is finished.
  118. - parameter URLRequest: The URL request.
  119. - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the
  120. image will not change its image until the image request finishes.
  121. Defaults to `nil`.
  122. - parameter completion: A closure to be executed when the image request finishes. The closure
  123. has no return value and takes three arguments: the original request,
  124. the response from the server and the result containing either the
  125. image or the error that occurred. If the image was returned from the
  126. image cache, the response will be `nil`. Defaults to `nil`.
  127. */
  128. public func af_setImageForState(
  129. state: UIControlState,
  130. URLRequest: URLRequestConvertible,
  131. placeholderImage: UIImage? = nil,
  132. completion: (Response<UIImage, NSError> -> Void)? = nil)
  133. {
  134. guard !isImageURLRequest(URLRequest, equalToActiveRequestURLForState: state) else { return }
  135. af_cancelImageRequestForState(state)
  136. let imageDownloader = af_imageDownloader ?? UIButton.af_sharedImageDownloader
  137. let imageCache = imageDownloader.imageCache
  138. // Use the image from the image cache if it exists
  139. if let image = imageCache?.imageForRequest(URLRequest.URLRequest, withAdditionalIdentifier: nil) {
  140. let response = Response<UIImage, NSError>(
  141. request: URLRequest.URLRequest,
  142. response: nil,
  143. data: nil,
  144. result: .Success(image)
  145. )
  146. completion?(response)
  147. setImage(image, forState: state)
  148. return
  149. }
  150. // Set the placeholder since we're going to have to download
  151. if let placeholderImage = placeholderImage { self.setImage(placeholderImage, forState: state) }
  152. // Generate a unique download id to check whether the active request has changed while downloading
  153. let downloadID = NSUUID().UUIDString
  154. // Download the image, then set the image for the control state
  155. let requestReceipt = imageDownloader.downloadImage(
  156. URLRequest: URLRequest,
  157. receiptID: downloadID,
  158. filter: nil,
  159. completion: { [weak self] response in
  160. guard let strongSelf = self else { return }
  161. completion?(response)
  162. guard
  163. strongSelf.isImageURLRequest(response.request, equalToActiveRequestURLForState: state) &&
  164. strongSelf.imageRequestReceiptForState(state)?.receiptID == downloadID
  165. else {
  166. return
  167. }
  168. if let image = response.result.value {
  169. strongSelf.setImage(image, forState: state)
  170. }
  171. strongSelf.setImageRequestReceipt(nil, forState: state)
  172. }
  173. )
  174. setImageRequestReceipt(requestReceipt, forState: state)
  175. }
  176. /**
  177. Cancels the active download request for the image, if one exists.
  178. */
  179. public func af_cancelImageRequestForState(state: UIControlState) {
  180. guard let receipt = imageRequestReceiptForState(state) else { return }
  181. let imageDownloader = af_imageDownloader ?? UIButton.af_sharedImageDownloader
  182. imageDownloader.cancelRequestForRequestReceipt(receipt)
  183. setImageRequestReceipt(nil, forState: state)
  184. }
  185. // MARK: - Background Image Downloads
  186. /**
  187. Asynchronously downloads an image from the specified URL and sets it once the request is finished.
  188. If the image is cached locally, the image is set immediately. Otherwise the specified placehoder image will be
  189. set immediately, and then the remote image will be set once the image request is finished.
  190. - parameter URL: The URL used for the image request.
  191. - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the
  192. background image will not change its image until the image request finishes.
  193. Defaults to `nil`.
  194. */
  195. public func af_setBackgroundImageForState(
  196. state: UIControlState,
  197. URL: NSURL,
  198. placeHolderImage: UIImage? = nil,
  199. completion: (Response<UIImage, NSError> -> Void)? = nil)
  200. {
  201. af_setBackgroundImageForState(state,
  202. URLRequest: URLRequestWithURL(URL),
  203. placeholderImage: placeHolderImage,
  204. completion: completion)
  205. }
  206. /**
  207. Asynchronously downloads an image from the specified URL request and sets it once the request is finished.
  208. If the image is cached locally, the image is set immediately. Otherwise the specified placehoder image will be
  209. set immediately, and then the remote image will be set once the image request is finished.
  210. - parameter URLRequest: The URL request.
  211. - parameter placeholderImage: The image to be set initially until the image request finished. If `nil`, the
  212. background image will not change its image until the image request finishes.
  213. Defaults to `nil`.
  214. - parameter completion: A closure to be executed when the image request finishes. The closure
  215. has no return value and takes three arguments: the original request,
  216. the response from the server and the result containing either the
  217. image or the error that occurred. If the image was returned from the
  218. image cache, the response will be `nil`. Defaults to `nil`.
  219. */
  220. public func af_setBackgroundImageForState(
  221. state: UIControlState,
  222. URLRequest: URLRequestConvertible,
  223. placeholderImage: UIImage? = nil,
  224. completion: (Response<UIImage, NSError> -> Void)? = nil)
  225. {
  226. guard !isImageURLRequest(URLRequest, equalToActiveRequestURLForState: state) else { return }
  227. af_cancelBackgroundImageRequestForState(state)
  228. let imageDownloader = af_imageDownloader ?? UIButton.af_sharedImageDownloader
  229. let imageCache = imageDownloader.imageCache
  230. // Use the image from the image cache if it exists
  231. if let image = imageCache?.imageForRequest(URLRequest.URLRequest, withAdditionalIdentifier: nil) {
  232. let response = Response<UIImage, NSError>(
  233. request: URLRequest.URLRequest,
  234. response: nil,
  235. data: nil,
  236. result: .Success(image)
  237. )
  238. completion?(response)
  239. setBackgroundImage(image, forState: state)
  240. return
  241. }
  242. // Set the placeholder since we're going to have to download
  243. if let placeholderImage = placeholderImage { self.setBackgroundImage(placeholderImage, forState: state) }
  244. // Generate a unique download id to check whether the active request has changed while downloading
  245. let downloadID = NSUUID().UUIDString
  246. // Download the image, then set the image for the control state
  247. let requestReceipt = imageDownloader.downloadImage(
  248. URLRequest: URLRequest,
  249. receiptID: downloadID,
  250. filter: nil,
  251. completion: { [weak self] response in
  252. guard let strongSelf = self else { return }
  253. completion?(response)
  254. guard
  255. strongSelf.isBackgroundImageURLRequest(response.request, equalToActiveRequestURLForState: state) &&
  256. strongSelf.backgroundImageRequestReceiptForState(state)?.receiptID == downloadID
  257. else {
  258. return
  259. }
  260. if let image = response.result.value {
  261. strongSelf.setBackgroundImage(image, forState: state)
  262. }
  263. strongSelf.setBackgroundImageRequestReceipt(nil, forState: state)
  264. }
  265. )
  266. setBackgroundImageRequestReceipt(requestReceipt, forState: state)
  267. }
  268. /**
  269. Cancels the active download request for the background image, if one exists.
  270. */
  271. public func af_cancelBackgroundImageRequestForState(state: UIControlState) {
  272. guard let receipt = backgroundImageRequestReceiptForState(state) else { return }
  273. let imageDownloader = af_imageDownloader ?? UIButton.af_sharedImageDownloader
  274. imageDownloader.cancelRequestForRequestReceipt(receipt)
  275. setBackgroundImageRequestReceipt(nil, forState: state)
  276. }
  277. // MARK: - Internal - Image Request Receipts
  278. func imageRequestReceiptForState(state: UIControlState) -> RequestReceipt? {
  279. guard let receipt = imageRequestReceipts[state.rawValue] else { return nil }
  280. return receipt
  281. }
  282. func setImageRequestReceipt(receipt: RequestReceipt?, forState state: UIControlState) {
  283. var receipts = imageRequestReceipts
  284. receipts[state.rawValue] = receipt
  285. imageRequestReceipts = receipts
  286. }
  287. // MARK: - Internal - Background Image Request Receipts
  288. func backgroundImageRequestReceiptForState(state: UIControlState) -> RequestReceipt? {
  289. guard let receipt = backgroundImageRequestReceipts[state.rawValue] else { return nil }
  290. return receipt
  291. }
  292. func setBackgroundImageRequestReceipt(receipt: RequestReceipt?, forState state: UIControlState) {
  293. var receipts = backgroundImageRequestReceipts
  294. receipts[state.rawValue] = receipt
  295. backgroundImageRequestReceipts = receipts
  296. }
  297. // MARK: - Private - URL Request Helpers
  298. private func isImageURLRequest(
  299. URLRequest: URLRequestConvertible?,
  300. equalToActiveRequestURLForState state: UIControlState)
  301. -> Bool
  302. {
  303. if let
  304. currentRequest = imageRequestReceiptForState(state)?.request.task.originalRequest
  305. where currentRequest.URLString == URLRequest?.URLRequest.URLString
  306. {
  307. return true
  308. }
  309. return false
  310. }
  311. private func isBackgroundImageURLRequest(
  312. URLRequest: URLRequestConvertible?,
  313. equalToActiveRequestURLForState state: UIControlState)
  314. -> Bool
  315. {
  316. if let
  317. currentRequest = backgroundImageRequestReceiptForState(state)?.request.task.originalRequest
  318. where currentRequest.URLString == URLRequest?.URLRequest.URLString
  319. {
  320. return true
  321. }
  322. return false
  323. }
  324. private func URLRequestWithURL(URL: NSURL) -> NSURLRequest {
  325. let mutableURLRequest = NSMutableURLRequest(URL: URL)
  326. for mimeType in Request.acceptableImageContentTypes {
  327. mutableURLRequest.addValue(mimeType, forHTTPHeaderField: "Accept")
  328. }
  329. return mutableURLRequest
  330. }
  331. }