NSButton+Kingfisher.swift 15 KB

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