NSButton+Kingfisher.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. //
  2. // NSButton+Kingfisher.swift
  3. // Kingfisher
  4. //
  5. // Created by Jie Zhang on 14/04/2016.
  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. import AppKit
  27. extension KingfisherWrapper where Base: NSButton {
  28. // MARK: Setting Image
  29. /// Sets an image to the button with a source.
  30. ///
  31. /// - Parameters:
  32. /// - source: The `Source` object contains information about how to get the image.
  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 source.
  42. /// 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(
  47. with source: Source?,
  48. placeholder: Image? = nil,
  49. options: KingfisherOptionsInfo? = nil,
  50. progressBlock: DownloadProgressBlock? = nil,
  51. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  52. {
  53. var mutatingSelf = self
  54. guard let source = source else {
  55. base.image = placeholder
  56. mutatingSelf.taskIdentifier = nil
  57. completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
  58. return nil
  59. }
  60. var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
  61. if !options.keepCurrentImageWhileLoading {
  62. base.image = placeholder
  63. }
  64. let issuedIdentifier = Source.Identifier.next()
  65. mutatingSelf.taskIdentifier = issuedIdentifier
  66. if let block = progressBlock {
  67. options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  68. }
  69. if let provider = ImageProgressiveProvider(options, refresh: { image in
  70. self.base.image = image
  71. }) {
  72. options.onDataReceived = (options.onDataReceived ?? []) + [provider]
  73. }
  74. options.onDataReceived?.forEach {
  75. $0.onShouldApply = { issuedIdentifier == self.taskIdentifier }
  76. }
  77. let task = KingfisherManager.shared.retrieveImage(
  78. with: source,
  79. options: options,
  80. completionHandler: { result in
  81. CallbackQueue.mainCurrentOrAsync.execute {
  82. guard issuedIdentifier == self.taskIdentifier else {
  83. let reason: KingfisherError.ImageSettingErrorReason
  84. do {
  85. let value = try result.get()
  86. reason = .notCurrentSourceTask(result: value, error: nil, source: source)
  87. } catch {
  88. reason = .notCurrentSourceTask(result: nil, error: error, source: source)
  89. }
  90. let error = KingfisherError.imageSettingError(reason: reason)
  91. completionHandler?(.failure(error))
  92. return
  93. }
  94. mutatingSelf.imageTask = nil
  95. mutatingSelf.taskIdentifier = nil
  96. switch result {
  97. case .success(let value):
  98. self.base.image = value.image
  99. completionHandler?(result)
  100. case .failure:
  101. if let image = options.onFailureImage {
  102. self.base.image = image
  103. }
  104. completionHandler?(result)
  105. }
  106. }
  107. }
  108. )
  109. mutatingSelf.imageTask = task
  110. return task
  111. }
  112. /// Sets an image to the button with a requested resource.
  113. ///
  114. /// - Parameters:
  115. /// - resource: The `Resource` object contains information about the resource.
  116. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  117. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  118. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  119. /// `expectedContentLength`, this block will not be called.
  120. /// - completionHandler: Called when the image retrieved and set finished.
  121. /// - Returns: A task represents the image downloading.
  122. ///
  123. /// - Note:
  124. /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
  125. /// or network. Since this method will perform UI changes, you must call it from the main thread.
  126. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  127. ///
  128. @discardableResult
  129. public func setImage(
  130. with resource: Resource?,
  131. placeholder: Image? = nil,
  132. options: KingfisherOptionsInfo? = nil,
  133. progressBlock: DownloadProgressBlock? = nil,
  134. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  135. {
  136. return setImage(
  137. with: resource.map { .network($0) },
  138. placeholder: placeholder,
  139. options: options,
  140. progressBlock: progressBlock,
  141. completionHandler: completionHandler)
  142. }
  143. // MARK: Cancelling Downloading Task
  144. /// Cancels the image download task of the button if it is running.
  145. /// Nothing will happen if the downloading has already finished.
  146. public func cancelImageDownloadTask() {
  147. imageTask?.cancel()
  148. }
  149. // MARK: Setting Alternate Image
  150. @discardableResult
  151. public func setAlternateImage(
  152. with source: Source?,
  153. placeholder: Image? = nil,
  154. options: KingfisherOptionsInfo? = nil,
  155. progressBlock: DownloadProgressBlock? = nil,
  156. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  157. {
  158. var mutatingSelf = self
  159. guard let source = source else {
  160. base.alternateImage = placeholder
  161. mutatingSelf.alternateTaskIdentifier = nil
  162. completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
  163. return nil
  164. }
  165. var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
  166. if !options.keepCurrentImageWhileLoading {
  167. base.alternateImage = placeholder
  168. }
  169. let issuedIdentifier = Source.Identifier.next()
  170. mutatingSelf.alternateTaskIdentifier = issuedIdentifier
  171. if let block = progressBlock {
  172. options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  173. }
  174. if let provider = ImageProgressiveProvider(options, refresh: { image in
  175. self.base.alternateImage = image
  176. }) {
  177. options.onDataReceived = (options.onDataReceived ?? []) + [provider]
  178. }
  179. options.onDataReceived?.forEach {
  180. $0.onShouldApply = { issuedIdentifier == self.alternateTaskIdentifier }
  181. }
  182. let task = KingfisherManager.shared.retrieveImage(
  183. with: source,
  184. options: options,
  185. completionHandler: { result in
  186. CallbackQueue.mainCurrentOrAsync.execute {
  187. guard issuedIdentifier == self.alternateTaskIdentifier else {
  188. let reason: KingfisherError.ImageSettingErrorReason
  189. do {
  190. let value = try result.get()
  191. reason = .notCurrentSourceTask(result: value, error: nil, source: source)
  192. } catch {
  193. reason = .notCurrentSourceTask(result: nil, error: error, source: source)
  194. }
  195. let error = KingfisherError.imageSettingError(reason: reason)
  196. completionHandler?(.failure(error))
  197. return
  198. }
  199. mutatingSelf.alternateImageTask = nil
  200. mutatingSelf.alternateTaskIdentifier = nil
  201. switch result {
  202. case .success(let value):
  203. self.base.alternateImage = value.image
  204. completionHandler?(result)
  205. case .failure:
  206. if let image = options.onFailureImage {
  207. self.base.alternateImage = image
  208. }
  209. completionHandler?(result)
  210. }
  211. }
  212. }
  213. )
  214. mutatingSelf.alternateImageTask = task
  215. return task
  216. }
  217. /// Sets an alternate image to the button with a requested resource.
  218. ///
  219. /// - Parameters:
  220. /// - resource: The `Resource` object contains information about the resource.
  221. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  222. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  223. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  224. /// `expectedContentLength`, this block will not be called.
  225. /// - completionHandler: Called when the image retrieved and set finished.
  226. /// - Returns: A task represents the image downloading.
  227. ///
  228. /// - Note:
  229. /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
  230. /// or network. Since this method will perform UI changes, you must call it from the main thread.
  231. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  232. ///
  233. @discardableResult
  234. public func setAlternateImage(
  235. with resource: Resource?,
  236. placeholder: Image? = nil,
  237. options: KingfisherOptionsInfo? = nil,
  238. progressBlock: DownloadProgressBlock? = nil,
  239. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  240. {
  241. return setAlternateImage(
  242. with: resource.map { .network($0) },
  243. placeholder: placeholder,
  244. options: options,
  245. progressBlock: progressBlock,
  246. completionHandler: completionHandler)
  247. }
  248. // MARK: Cancelling Alternate Image Downloading Task
  249. /// Cancels the alternate image download task of the button if it is running.
  250. /// Nothing will happen if the downloading has already finished.
  251. public func cancelAlternateImageDownloadTask() {
  252. alternateImageTask?.cancel()
  253. }
  254. }
  255. // MARK: - Associated Object
  256. private var taskIdentifierKey: Void?
  257. private var imageTaskKey: Void?
  258. private var alternateTaskIdentifierKey: Void?
  259. private var alternateImageTaskKey: Void?
  260. extension KingfisherWrapper where Base: NSButton {
  261. // MARK: Properties
  262. public private(set) var taskIdentifier: Source.Identifier.Value? {
  263. get {
  264. let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
  265. return box?.value
  266. }
  267. set {
  268. let box = newValue.map { Box($0) }
  269. setRetainedAssociatedObject(base, &taskIdentifierKey, box)
  270. }
  271. }
  272. private var imageTask: DownloadTask? {
  273. get { return getAssociatedObject(base, &imageTaskKey) }
  274. set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
  275. }
  276. public private(set) var alternateTaskIdentifier: Source.Identifier.Value? {
  277. get {
  278. let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &alternateTaskIdentifierKey)
  279. return box?.value
  280. }
  281. set {
  282. let box = newValue.map { Box($0) }
  283. setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box)
  284. }
  285. }
  286. private var alternateImageTask: DownloadTask? {
  287. get { return getAssociatedObject(base, &alternateImageTaskKey) }
  288. set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)}
  289. }
  290. }
  291. extension KingfisherWrapper where Base: NSButton {
  292. /// Gets the image URL bound to this button.
  293. @available(*, deprecated, message: "Use `taskIdentifier` instead to identify a setting task.")
  294. public private(set) var webURL: URL? {
  295. get { return nil }
  296. set { }
  297. }
  298. /// Gets the image URL bound to this button.
  299. @available(*, deprecated, message: "Use `alternateTaskIdentifier` instead to identify a setting task.")
  300. public private(set) var alternateWebURL: URL? {
  301. get { return nil }
  302. set { }
  303. }
  304. }