UIButton+Kingfisher.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. //
  2. // UIButton+Kingfisher.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/13.
  6. //
  7. // Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import UIKit
  27. extension KingfisherClass where Base: UIButton {
  28. /// Sets an image to the button for a specified state with a requested resource.
  29. ///
  30. /// - Parameters:
  31. /// - resource: The `Resource` object contains information about the resource.
  32. /// - state: The button state to which the image should be set.
  33. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  34. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  35. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  36. /// `expectedContentLength`, this block will not be called.
  37. /// - completionHandler: Called when the image retrieved and set finished.
  38. /// - Returns: A task represents the image downloading.
  39. ///
  40. /// - Note:
  41. /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
  42. /// or network. Since this method will perform UI changes, you must call it from the main thread.
  43. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  44. ///
  45. @discardableResult
  46. public func setImage(with resource: Resource?,
  47. for state: UIControl.State,
  48. placeholder: UIImage? = nil,
  49. options: KingfisherOptionsInfo? = nil,
  50. progressBlock: DownloadProgressBlock? = nil,
  51. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil)
  52. -> DownloadTask?
  53. {
  54. guard let resource = resource else {
  55. base.setImage(placeholder, for: state)
  56. setWebURL(nil, for: state)
  57. completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptyResource)))
  58. return nil
  59. }
  60. let options = KingfisherManager.shared.defaultOptions + (options ?? .empty)
  61. if !options.keepCurrentImageWhileLoading {
  62. base.setImage(placeholder, for: state)
  63. }
  64. setWebURL(resource.downloadURL, for: state)
  65. let task = KingfisherManager.shared.retrieveImage(
  66. with: resource,
  67. options: options,
  68. progressBlock: { receivedSize, totalSize in
  69. guard resource.downloadURL == self.webURL(for: state) else { return }
  70. progressBlock?(receivedSize, totalSize)
  71. },
  72. completionHandler: { result in
  73. DispatchQueue.main.safeAsync {
  74. guard resource.downloadURL == self.webURL(for: state) else {
  75. let error = KingfisherError.imageSettingError(
  76. reason: .notCurrentSource(result: result.value, error: result.error, source: .network(resource)))
  77. completionHandler?(.failure(error))
  78. return
  79. }
  80. self.imageTask = nil
  81. switch result {
  82. case .success(let value):
  83. self.base.setImage(value.image, for: state)
  84. completionHandler?(result)
  85. case .failure:
  86. if let image = options.onFailureImage {
  87. self.base.setImage(image, for: state)
  88. }
  89. completionHandler?(result)
  90. }
  91. }
  92. })
  93. imageTask = task
  94. return task
  95. }
  96. /// Cancels the image download task of the button if it is running.
  97. /// Nothing will happen if the downloading has already finished.
  98. public func cancelImageDownloadTask() {
  99. imageTask?.cancel()
  100. }
  101. /// Sets a background image to the button for a specified state with a requested resource.
  102. ///
  103. /// - Parameters:
  104. /// - resource: The `Resource` object contains information about the resource.
  105. /// - state: The button state to which the image should be set.
  106. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  107. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  108. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  109. /// `expectedContentLength`, this block will not be called.
  110. /// - completionHandler: Called when the image retrieved and set finished.
  111. /// - Returns: A task represents the image downloading.
  112. ///
  113. /// - Note:
  114. /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
  115. /// or network. Since this method will perform UI changes, you must call it from the main thread.
  116. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  117. ///
  118. @discardableResult
  119. public func setBackgroundImage(with resource: Resource?,
  120. for state: UIControl.State,
  121. placeholder: UIImage? = nil,
  122. options: KingfisherOptionsInfo? = nil,
  123. progressBlock: DownloadProgressBlock? = nil,
  124. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil)
  125. -> DownloadTask?
  126. {
  127. guard let resource = resource else {
  128. base.setBackgroundImage(placeholder, for: state)
  129. setBackgroundWebURL(nil, for: state)
  130. completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptyResource)))
  131. return nil
  132. }
  133. let options = KingfisherManager.shared.defaultOptions + (options ?? .empty)
  134. if !options.keepCurrentImageWhileLoading {
  135. base.setBackgroundImage(placeholder, for: state)
  136. }
  137. setBackgroundWebURL(resource.downloadURL, for: state)
  138. let task = KingfisherManager.shared.retrieveImage(
  139. with: resource,
  140. options: options,
  141. progressBlock: { receivedSize, totalSize in
  142. guard resource.downloadURL == self.backgroundWebURL(for: state) else {
  143. return
  144. }
  145. if let progressBlock = progressBlock {
  146. progressBlock(receivedSize, totalSize)
  147. }
  148. },
  149. completionHandler: { result in
  150. DispatchQueue.main.safeAsync {
  151. guard resource.downloadURL == self.backgroundWebURL(for: state) else {
  152. let error = KingfisherError.imageSettingError(
  153. reason: .notCurrentSource(result: result.value, error: result.error, source: .network(resource)))
  154. completionHandler?(.failure(error))
  155. return
  156. }
  157. self.backgroundImageTask = nil
  158. switch result {
  159. case .success(let value):
  160. self.base.setBackgroundImage(value.image, for: state)
  161. completionHandler?(result)
  162. case .failure:
  163. if let image = options.onFailureImage {
  164. self.base.setBackgroundImage(image, for: state)
  165. }
  166. completionHandler?(result)
  167. }
  168. }
  169. })
  170. backgroundImageTask = task
  171. return task
  172. }
  173. /// Cancels the background image download task of the button if it is running.
  174. /// Nothing will happen if the downloading has already finished.
  175. public func cancelBackgroundImageDownloadTask() {
  176. backgroundImageTask?.cancel()
  177. }
  178. }
  179. // MARK: - Associated Object
  180. private var lastURLKey: Void?
  181. private var imageTaskKey: Void?
  182. extension KingfisherClass where Base: UIButton {
  183. /// Gets the image URL of this button for a specified state.
  184. ///
  185. /// - Parameter state: The state that uses the specified image.
  186. /// - Returns: Current URL for image.
  187. public func webURL(for state: UIControl.State) -> URL? {
  188. return webURLs[NSNumber(value:state.rawValue)] as? URL
  189. }
  190. private func setWebURL(_ url: URL?, for state: UIControl.State) {
  191. webURLs[NSNumber(value:state.rawValue)] = url
  192. }
  193. private var webURLs: NSMutableDictionary {
  194. get {
  195. guard let dictionary: NSMutableDictionary = getAssociatedObject(base, &lastURLKey) else {
  196. let dic = NSMutableDictionary()
  197. self.webURLs = dic
  198. return dic
  199. }
  200. return dictionary
  201. }
  202. set {
  203. setRetainedAssociatedObject(base, &lastURLKey, newValue)
  204. }
  205. }
  206. private var imageTask: DownloadTask? {
  207. get { return getAssociatedObject(base, &imageTaskKey) }
  208. set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
  209. }
  210. }
  211. private var lastBackgroundURLKey: Void?
  212. private var backgroundImageTaskKey: Void?
  213. extension KingfisherClass where Base: UIButton {
  214. /// Gets the background image URL of this button for a specified state.
  215. ///
  216. /// - Parameter state: The state that uses the specified background image.
  217. /// - Returns: Current URL for image.
  218. public func backgroundWebURL(for state: UIControl.State) -> URL? {
  219. return backgroundWebURLs[NSNumber(value:state.rawValue)] as? URL
  220. }
  221. private func setBackgroundWebURL(_ url: URL?, for state: UIControl.State) {
  222. backgroundWebURLs[NSNumber(value:state.rawValue)] = url
  223. }
  224. private var backgroundWebURLs: NSMutableDictionary {
  225. get {
  226. guard let dictionary: NSMutableDictionary = getAssociatedObject(base, &lastBackgroundURLKey) else {
  227. let dic = NSMutableDictionary()
  228. self.backgroundWebURLs = dic
  229. return dic
  230. }
  231. return dictionary
  232. }
  233. set {
  234. setRetainedAssociatedObject(base, &lastBackgroundURLKey, newValue)
  235. }
  236. }
  237. private var backgroundImageTask: DownloadTask? {
  238. get { return getAssociatedObject(base, &backgroundImageTaskKey) }
  239. set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) }
  240. }
  241. }