You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

398 lines
17 KiB

  1. //
  2. // UIButton+Kingfisher.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/13.
  6. //
  7. // Copyright (c) 2019 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. #if !os(watchOS)
  27. #if canImport(UIKit)
  28. import UIKit
  29. extension KingfisherWrapper where Base: UIButton {
  30. // MARK: Setting Image
  31. /// Sets an image to the button for a specified state with a source.
  32. ///
  33. /// - Parameters:
  34. /// - source: The `Source` object contains information about the image.
  35. /// - state: The button state to which the image should be set.
  36. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  37. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  38. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  39. /// `expectedContentLength`, this block will not be called.
  40. /// - completionHandler: Called when the image retrieved and set finished.
  41. /// - Returns: A task represents the image downloading.
  42. ///
  43. /// - Note:
  44. /// Internally, this method will use `KingfisherManager` to get the requested source, from either cache
  45. /// or network. Since this method will perform UI changes, you must call it from the main thread.
  46. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  47. ///
  48. @discardableResult
  49. public func setImage(
  50. with source: Source?,
  51. for state: UIControl.State,
  52. placeholder: UIImage? = nil,
  53. options: KingfisherOptionsInfo? = nil,
  54. progressBlock: DownloadProgressBlock? = nil,
  55. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  56. {
  57. guard let source = source else {
  58. base.setImage(placeholder, for: state)
  59. setTaskIdentifier(nil, for: state)
  60. completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
  61. return nil
  62. }
  63. var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
  64. if !options.keepCurrentImageWhileLoading {
  65. base.setImage(placeholder, for: state)
  66. }
  67. var mutatingSelf = self
  68. let issuedIdentifier = Source.Identifier.next()
  69. setTaskIdentifier(issuedIdentifier, for: state)
  70. if let block = progressBlock {
  71. options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  72. }
  73. if let provider = ImageProgressiveProvider(options, refresh: { image in
  74. self.base.setImage(image, for: state)
  75. }) {
  76. options.onDataReceived = (options.onDataReceived ?? []) + [provider]
  77. }
  78. options.onDataReceived?.forEach {
  79. $0.onShouldApply = { issuedIdentifier == self.taskIdentifier(for: state) }
  80. }
  81. let task = KingfisherManager.shared.retrieveImage(
  82. with: source,
  83. options: options,
  84. downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
  85. completionHandler: { result in
  86. CallbackQueue.mainCurrentOrAsync.execute {
  87. guard issuedIdentifier == self.taskIdentifier(for: state) else {
  88. let reason: KingfisherError.ImageSettingErrorReason
  89. do {
  90. let value = try result.get()
  91. reason = .notCurrentSourceTask(result: value, error: nil, source: source)
  92. } catch {
  93. reason = .notCurrentSourceTask(result: nil, error: error, source: source)
  94. }
  95. let error = KingfisherError.imageSettingError(reason: reason)
  96. completionHandler?(.failure(error))
  97. return
  98. }
  99. mutatingSelf.imageTask = nil
  100. mutatingSelf.setTaskIdentifier(nil, for: state)
  101. switch result {
  102. case .success(let value):
  103. self.base.setImage(value.image, for: state)
  104. completionHandler?(result)
  105. case .failure:
  106. if let image = options.onFailureImage {
  107. self.base.setImage(image, for: state)
  108. }
  109. completionHandler?(result)
  110. }
  111. }
  112. }
  113. )
  114. mutatingSelf.imageTask = task
  115. return task
  116. }
  117. /// Sets an image to the button for a specified state with a requested resource.
  118. ///
  119. /// - Parameters:
  120. /// - resource: The `Resource` object contains information about the resource.
  121. /// - state: The button state to which the image should be set.
  122. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  123. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  124. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  125. /// `expectedContentLength`, this block will not be called.
  126. /// - completionHandler: Called when the image retrieved and set finished.
  127. /// - Returns: A task represents the image downloading.
  128. ///
  129. /// - Note:
  130. /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
  131. /// or network. Since this method will perform UI changes, you must call it from the main thread.
  132. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  133. ///
  134. @discardableResult
  135. public func setImage(
  136. with resource: Resource?,
  137. for state: UIControl.State,
  138. placeholder: UIImage? = nil,
  139. options: KingfisherOptionsInfo? = nil,
  140. progressBlock: DownloadProgressBlock? = nil,
  141. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  142. {
  143. return setImage(
  144. with: resource?.convertToSource(),
  145. for: state,
  146. placeholder: placeholder,
  147. options: options,
  148. progressBlock: progressBlock,
  149. completionHandler: completionHandler)
  150. }
  151. // MARK: Cancelling Downloading Task
  152. /// Cancels the image download task of the button if it is running.
  153. /// Nothing will happen if the downloading has already finished.
  154. public func cancelImageDownloadTask() {
  155. imageTask?.cancel()
  156. }
  157. // MARK: Setting Background Image
  158. /// Sets a background image to the button for a specified state with a source.
  159. ///
  160. /// - Parameters:
  161. /// - source: The `Source` object contains information about the image.
  162. /// - state: The button state to which the image should be set.
  163. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  164. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  165. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  166. /// `expectedContentLength`, this block will not be called.
  167. /// - completionHandler: Called when the image retrieved and set finished.
  168. /// - Returns: A task represents the image downloading.
  169. ///
  170. /// - Note:
  171. /// Internally, this method will use `KingfisherManager` to get the requested source
  172. /// Since this method will perform UI changes, you must call it from the main thread.
  173. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  174. ///
  175. @discardableResult
  176. public func setBackgroundImage(
  177. with source: Source?,
  178. for state: UIControl.State,
  179. placeholder: UIImage? = nil,
  180. options: KingfisherOptionsInfo? = nil,
  181. progressBlock: DownloadProgressBlock? = nil,
  182. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  183. {
  184. guard let source = source else {
  185. base.setBackgroundImage(placeholder, for: state)
  186. setBackgroundTaskIdentifier(nil, for: state)
  187. completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
  188. return nil
  189. }
  190. var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
  191. if !options.keepCurrentImageWhileLoading {
  192. base.setBackgroundImage(placeholder, for: state)
  193. }
  194. var mutatingSelf = self
  195. let issuedIdentifier = Source.Identifier.next()
  196. setBackgroundTaskIdentifier(issuedIdentifier, for: state)
  197. if let block = progressBlock {
  198. options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  199. }
  200. if let provider = ImageProgressiveProvider(options, refresh: { image in
  201. self.base.setBackgroundImage(image, for: state)
  202. }) {
  203. options.onDataReceived = (options.onDataReceived ?? []) + [provider]
  204. }
  205. options.onDataReceived?.forEach {
  206. $0.onShouldApply = { issuedIdentifier == self.backgroundTaskIdentifier(for: state) }
  207. }
  208. let task = KingfisherManager.shared.retrieveImage(
  209. with: source,
  210. options: options,
  211. downloadTaskUpdated: { mutatingSelf.backgroundImageTask = $0 },
  212. completionHandler: { result in
  213. CallbackQueue.mainCurrentOrAsync.execute {
  214. guard issuedIdentifier == self.backgroundTaskIdentifier(for: state) else {
  215. let reason: KingfisherError.ImageSettingErrorReason
  216. do {
  217. let value = try result.get()
  218. reason = .notCurrentSourceTask(result: value, error: nil, source: source)
  219. } catch {
  220. reason = .notCurrentSourceTask(result: nil, error: error, source: source)
  221. }
  222. let error = KingfisherError.imageSettingError(reason: reason)
  223. completionHandler?(.failure(error))
  224. return
  225. }
  226. mutatingSelf.backgroundImageTask = nil
  227. mutatingSelf.setBackgroundTaskIdentifier(nil, for: state)
  228. switch result {
  229. case .success(let value):
  230. self.base.setBackgroundImage(value.image, for: state)
  231. completionHandler?(result)
  232. case .failure:
  233. if let image = options.onFailureImage {
  234. self.base.setBackgroundImage(image, for: state)
  235. }
  236. completionHandler?(result)
  237. }
  238. }
  239. }
  240. )
  241. mutatingSelf.backgroundImageTask = task
  242. return task
  243. }
  244. /// Sets a background image to the button for a specified state with a requested resource.
  245. ///
  246. /// - Parameters:
  247. /// - resource: The `Resource` object contains information about the resource.
  248. /// - state: The button state to which the image should be set.
  249. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  250. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  251. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  252. /// `expectedContentLength`, this block will not be called.
  253. /// - completionHandler: Called when the image retrieved and set finished.
  254. /// - Returns: A task represents the image downloading.
  255. ///
  256. /// - Note:
  257. /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
  258. /// or network. Since this method will perform UI changes, you must call it from the main thread.
  259. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  260. ///
  261. @discardableResult
  262. public func setBackgroundImage(
  263. with resource: Resource?,
  264. for state: UIControl.State,
  265. placeholder: UIImage? = nil,
  266. options: KingfisherOptionsInfo? = nil,
  267. progressBlock: DownloadProgressBlock? = nil,
  268. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  269. {
  270. return setBackgroundImage(
  271. with: resource?.convertToSource(),
  272. for: state,
  273. placeholder: placeholder,
  274. options: options,
  275. progressBlock: progressBlock,
  276. completionHandler: completionHandler)
  277. }
  278. // MARK: Cancelling Background Downloading Task
  279. /// Cancels the background image download task of the button if it is running.
  280. /// Nothing will happen if the downloading has already finished.
  281. public func cancelBackgroundImageDownloadTask() {
  282. backgroundImageTask?.cancel()
  283. }
  284. }
  285. // MARK: - Associated Object
  286. private var taskIdentifierKey: Void?
  287. private var imageTaskKey: Void?
  288. // MARK: Properties
  289. extension KingfisherWrapper where Base: UIButton {
  290. private typealias TaskIdentifier = Box<[UInt: Source.Identifier.Value]>
  291. public func taskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? {
  292. return taskIdentifierInfo.value[state.rawValue]
  293. }
  294. private func setTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) {
  295. taskIdentifierInfo.value[state.rawValue] = identifier
  296. }
  297. private var taskIdentifierInfo: TaskIdentifier {
  298. return getAssociatedObject(base, &taskIdentifierKey) ?? {
  299. setRetainedAssociatedObject(base, &taskIdentifierKey, $0)
  300. return $0
  301. } (TaskIdentifier([:]))
  302. }
  303. private var imageTask: DownloadTask? {
  304. get { return getAssociatedObject(base, &imageTaskKey) }
  305. set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
  306. }
  307. }
  308. private var backgroundTaskIdentifierKey: Void?
  309. private var backgroundImageTaskKey: Void?
  310. // MARK: Background Properties
  311. extension KingfisherWrapper where Base: UIButton {
  312. public func backgroundTaskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? {
  313. return backgroundTaskIdentifierInfo.value[state.rawValue]
  314. }
  315. private func setBackgroundTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) {
  316. backgroundTaskIdentifierInfo.value[state.rawValue] = identifier
  317. }
  318. private var backgroundTaskIdentifierInfo: TaskIdentifier {
  319. return getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? {
  320. setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0)
  321. return $0
  322. } (TaskIdentifier([:]))
  323. }
  324. private var backgroundImageTask: DownloadTask? {
  325. get { return getAssociatedObject(base, &backgroundImageTaskKey) }
  326. mutating set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) }
  327. }
  328. }
  329. extension KingfisherWrapper where Base: UIButton {
  330. /// Gets the image URL of this button for a specified state.
  331. ///
  332. /// - Parameter state: The state that uses the specified image.
  333. /// - Returns: Current URL for image.
  334. @available(*, deprecated, message: "Use `taskIdentifier` instead to identify a setting task.")
  335. public func webURL(for state: UIControl.State) -> URL? {
  336. return nil
  337. }
  338. /// Gets the background image URL of this button for a specified state.
  339. ///
  340. /// - Parameter state: The state that uses the specified background image.
  341. /// - Returns: Current URL for image.
  342. @available(*, deprecated, message: "Use `backgroundTaskIdentifier` instead to identify a setting task.")
  343. public func backgroundWebURL(for state: UIControl.State) -> URL? {
  344. return nil
  345. }
  346. }
  347. #endif
  348. #endif