NSButton+Kingfisher.swift 15 KB

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