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.

355 lines
14 KiB

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