KF.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. //
  2. // KF.swift
  3. // Kingfisher
  4. //
  5. // Created by onevcat on 2020/09/21.
  6. //
  7. // Copyright (c) 2020 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(UIKit)
  27. import UIKit
  28. #endif
  29. #if canImport(CarPlay) && !targetEnvironment(macCatalyst)
  30. import CarPlay
  31. #endif
  32. #if canImport(AppKit) && !targetEnvironment(macCatalyst)
  33. import AppKit
  34. #endif
  35. #if canImport(WatchKit)
  36. import WatchKit
  37. #endif
  38. #if canImport(TVUIKit)
  39. import TVUIKit
  40. #endif
  41. /// A helper type to create image setting tasks in a builder pattern.
  42. ///
  43. /// Use methods in this type to create a ``KF/Builder`` instance and configure image tasks there.
  44. public enum KF {
  45. /// Creates a builder for a given ``Source``.
  46. /// - Parameter source: The ``Source`` object defines data information from network or a data provider.
  47. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its
  48. /// `Builder/set(to:)` to start the image loading.
  49. public static func source(_ source: Source?) -> KF.Builder {
  50. Builder(source: source)
  51. }
  52. /// Creates a builder for a given ``Resource``.
  53. /// - Parameter resource: The ``Resource`` object defines data information like key or URL.
  54. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its
  55. /// `Builder/set(to:)` to start the image loading.
  56. public static func resource(_ resource: Resource?) -> KF.Builder {
  57. source(resource?.convertToSource())
  58. }
  59. /// Creates a builder for a given `URL` and an optional cache key.
  60. /// - Parameters:
  61. /// - url: The URL where the image should be downloaded.
  62. /// - cacheKey: The key used to store the downloaded image in cache.
  63. /// If `nil`, the `absoluteString` of `url` is used as the cache key.
  64. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its
  65. /// `Builder/set(to:)` to start the image loading.
  66. public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder {
  67. source(url?.convertToSource(overrideCacheKey: cacheKey))
  68. }
  69. /// Creates a builder for a given ``ImageDataProvider``.
  70. /// - Parameter provider: The ``ImageDataProvider`` object contains information about the data.
  71. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its
  72. /// `Builder/set(to:)` to start the image loading.
  73. public static func dataProvider(_ provider: ImageDataProvider?) -> KF.Builder {
  74. source(provider?.convertToSource())
  75. }
  76. /// Creates a builder for some given raw data and a cache key.
  77. /// - Parameters:
  78. /// - data: The data object from which the image should be created.
  79. /// - cacheKey: The key used to store the downloaded image in cache.
  80. /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its
  81. /// `Builder/set(to:)` to start the image loading.
  82. public static func data(_ data: Data?, cacheKey: String) -> KF.Builder {
  83. if let data = data {
  84. return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey))
  85. } else {
  86. return dataProvider(nil)
  87. }
  88. }
  89. }
  90. extension KF {
  91. /// A builder class to configure an image retrieving task and set it to a holder view or component.
  92. public class Builder: @unchecked Sendable {
  93. private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.KF.Builder.propertyQueue")
  94. private let source: Source?
  95. #if os(watchOS)
  96. private var _placeholder: KFCrossPlatformImage?
  97. private var placeholder: KFCrossPlatformImage? {
  98. get { propertyQueue.sync { _placeholder } }
  99. set { propertyQueue.sync { _placeholder = newValue } }
  100. }
  101. #else
  102. private var _placeholder: Placeholder?
  103. private var placeholder: Placeholder? {
  104. get { propertyQueue.sync { _placeholder } }
  105. set { propertyQueue.sync { _placeholder = newValue } }
  106. }
  107. #endif
  108. private var _options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions)
  109. public var options: KingfisherParsedOptionsInfo {
  110. get { propertyQueue.sync { _options } }
  111. set { propertyQueue.sync { _options = newValue } }
  112. }
  113. public let onFailureDelegate = Delegate<KingfisherError, Void>()
  114. public let onSuccessDelegate = Delegate<RetrieveImageResult, Void>()
  115. public let onProgressDelegate = Delegate<(Int64, Int64), Void>()
  116. init(source: Source?) {
  117. self.source = source
  118. }
  119. private var resultHandler: (@Sendable (Result<RetrieveImageResult, KingfisherError>) -> Void)? {
  120. {
  121. switch $0 {
  122. case .success(let result):
  123. self.onSuccessDelegate(result)
  124. case .failure(let error):
  125. self.onFailureDelegate(error)
  126. }
  127. }
  128. }
  129. private var progressBlock: DownloadProgressBlock? {
  130. onProgressDelegate.isSet ? { self.onProgressDelegate(($0, $1)) } : nil
  131. }
  132. }
  133. }
  134. @MainActor
  135. extension KF.Builder {
  136. #if !os(watchOS)
  137. /// Builds the image task request and sets it to an image view.
  138. /// - Parameter imageView: The image view which loads the task and should be set with the image.
  139. /// - Returns: A task represents the image downloading, if initialized.
  140. /// This value is `nil` if the image is being loaded from cache.
  141. @discardableResult
  142. public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? {
  143. imageView.kf.setImage(
  144. with: source,
  145. placeholder: placeholder,
  146. parsedOptions: options,
  147. progressBlock: progressBlock,
  148. completionHandler: resultHandler
  149. )
  150. }
  151. /// Builds the image task request and sets it to an `NSTextAttachment` object.
  152. /// - Parameters:
  153. /// - attachment: The text attachment object which loads the task and should be set with the image.
  154. /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added.
  155. /// - Returns: A task represents the image downloading, if initialized.
  156. /// This value is `nil` if the image is being loaded from cache.
  157. @discardableResult
  158. public func set(
  159. to attachment: NSTextAttachment,
  160. attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView) -> DownloadTask? {
  161. let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
  162. return attachment.kf.setImage(
  163. with: source,
  164. attributedView: attributedView,
  165. placeholder: placeholderImage,
  166. parsedOptions: options,
  167. progressBlock: progressBlock,
  168. completionHandler: resultHandler
  169. )
  170. }
  171. #if canImport(UIKit)
  172. /// Builds the image task request and sets it to a button.
  173. /// - Parameters:
  174. /// - button: The button which loads the task and should be set with the image.
  175. /// - state: The button state to which the image should be set.
  176. /// - Returns: A task represents the image downloading, if initialized.
  177. /// This value is `nil` if the image is being loaded from cache.
  178. @discardableResult
  179. public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? {
  180. let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
  181. return button.kf.setImage(
  182. with: source,
  183. for: state,
  184. placeholder: placeholderImage,
  185. parsedOptions: options,
  186. progressBlock: progressBlock,
  187. completionHandler: resultHandler
  188. )
  189. }
  190. /// Builds the image task request and sets it to the background image for a button.
  191. /// - Parameters:
  192. /// - button: The button which loads the task and should be set with the image.
  193. /// - state: The button state to which the image should be set.
  194. /// - Returns: A task represents the image downloading, if initialized.
  195. /// This value is `nil` if the image is being loaded from cache.
  196. @discardableResult
  197. public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? {
  198. let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
  199. return button.kf.setBackgroundImage(
  200. with: source,
  201. for: state,
  202. placeholder: placeholderImage,
  203. parsedOptions: options,
  204. progressBlock: progressBlock,
  205. completionHandler: resultHandler
  206. )
  207. }
  208. #endif // end of canImport(UIKit)
  209. #if canImport(CarPlay) && !targetEnvironment(macCatalyst)
  210. /// Builds the image task request and sets it to the image for a list item.
  211. /// - Parameters:
  212. /// - listItem: The list item which loads the task and should be set with the image.
  213. /// - Returns: A task represents the image downloading, if initialized.
  214. /// This value is `nil` if the image is being loaded from cache.
  215. @available(iOS 14.0, *)
  216. @discardableResult
  217. public func set(to listItem: CPListItem) -> DownloadTask? {
  218. let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
  219. return listItem.kf.setImage(
  220. with: source,
  221. placeholder: placeholderImage,
  222. parsedOptions: options,
  223. progressBlock: progressBlock,
  224. completionHandler: resultHandler
  225. )
  226. }
  227. #endif
  228. #if canImport(AppKit) && !targetEnvironment(macCatalyst)
  229. /// Builds the image task request and sets it to a button.
  230. /// - Parameter button: The button which loads the task and should be set with the image.
  231. /// - Returns: A task represents the image downloading, if initialized.
  232. /// This value is `nil` if the image is being loaded from cache.
  233. @discardableResult
  234. public func set(to button: NSButton) -> DownloadTask? {
  235. let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
  236. return button.kf.setImage(
  237. with: source,
  238. placeholder: placeholderImage,
  239. parsedOptions: options,
  240. progressBlock: progressBlock,
  241. completionHandler: resultHandler
  242. )
  243. }
  244. /// Builds the image task request and sets it to the alternative image for a button.
  245. /// - Parameter button: The button which loads the task and should be set with the image.
  246. /// - Returns: A task represents the image downloading, if initialized.
  247. /// This value is `nil` if the image is being loaded from cache.
  248. @discardableResult
  249. public func setAlternative(to button: NSButton) -> DownloadTask? {
  250. let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
  251. return button.kf.setAlternateImage(
  252. with: source,
  253. placeholder: placeholderImage,
  254. parsedOptions: options,
  255. progressBlock: progressBlock,
  256. completionHandler: resultHandler
  257. )
  258. }
  259. #endif // end of canImport(AppKit)
  260. #endif // end of !os(watchOS)
  261. #if canImport(WatchKit)
  262. /// Builds the image task request and sets it to a `WKInterfaceImage` object.
  263. /// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image.
  264. /// - Returns: A task represents the image downloading, if initialized.
  265. /// This value is `nil` if the image is being loaded from cache.
  266. @discardableResult
  267. public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? {
  268. return interfaceImage.kf.setImage(
  269. with: source,
  270. placeholder: placeholder,
  271. parsedOptions: options,
  272. progressBlock: progressBlock,
  273. completionHandler: resultHandler
  274. )
  275. }
  276. #endif // end of canImport(WatchKit)
  277. #if canImport(TVUIKit)
  278. /// Builds the image task request and sets it to a TV monogram view.
  279. /// - Parameter monogramView: The monogram view which loads the task and should be set with the image.
  280. /// - Returns: A task represents the image downloading, if initialized.
  281. /// This value is `nil` if the image is being loaded from cache.
  282. @available(tvOS 12.0, *)
  283. @discardableResult
  284. public func set(to monogramView: TVMonogramView) -> DownloadTask? {
  285. let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
  286. return monogramView.kf.setImage(
  287. with: source,
  288. placeholder: placeholderImage,
  289. parsedOptions: options,
  290. progressBlock: progressBlock,
  291. completionHandler: resultHandler
  292. )
  293. }
  294. #endif // end of canImport(TVUIKit)
  295. }
  296. #if !os(watchOS)
  297. extension KF.Builder {
  298. #if os(iOS) || os(tvOS) || os(visionOS)
  299. /// Sets a placeholder which is used while retrieving the image.
  300. /// - Parameter placeholder: A placeholder to show while retrieving the image from its source.
  301. /// - Returns: A ``KF/Builder`` with changes applied.
  302. public func placeholder(_ placeholder: Placeholder?) -> Self {
  303. self.placeholder = placeholder
  304. return self
  305. }
  306. #endif
  307. /// Sets a placeholder image which is used while retrieving the image.
  308. /// - Parameter placeholder: An image to show while retrieving the image from its source.
  309. /// - Returns: A ``KF/Builder`` with changes applied.
  310. public func placeholder(_ image: KFCrossPlatformImage?) -> Self {
  311. self.placeholder = image
  312. return self
  313. }
  314. }
  315. #endif
  316. extension KF.Builder {
  317. #if os(iOS) || os(tvOS) || os(visionOS)
  318. /// Sets the transition for the image task.
  319. /// - Parameter transition: The desired transition effect when setting the image to image view.
  320. /// - Returns: A ``KF/Builder`` with changes applied.
  321. ///
  322. /// Kingfisher will use the `transition` parameter to animate the image in if it is downloaded from web.
  323. /// The transition will not happen when the image is retrieved from either memory or disk cache by default.
  324. /// If you need to do the transition even when the image being retrieved from cache, also call
  325. /// ``KFOptionSetter/forceRefresh(_:)`` on the returned ``KF/Builder``.
  326. public func transition(_ transition: ImageTransition) -> Self {
  327. options.transition = transition
  328. return self
  329. }
  330. /// Sets a fade transition for the image task.
  331. /// - Parameter duration: The duration of the fade transition.
  332. /// - Returns: A ``KF/Builder`` with changes applied.
  333. ///
  334. /// Kingfisher will use the `transition` parameter to animate the image in if it is downloaded from web.
  335. /// The transition will not happen when the image is retrieved from either memory or disk cache by default.
  336. /// If you need to do the transition even when the image being retrieved from cache, also call
  337. /// ``KFOptionSetter/forceRefresh(_:)`` on the returned ``KF/Builder``.
  338. public func fade(duration: TimeInterval) -> Self {
  339. options.transition = .fade(duration)
  340. return self
  341. }
  342. #endif
  343. /// Sets whether keeping the existing image of image view while setting another image to it.
  344. /// - Parameter enabled: Whether the existing image should be kept.
  345. /// - Returns: A ``KF/Builder`` with changes applied.
  346. ///
  347. /// By setting this option, the placeholder image parameter of image view extension method
  348. /// will be ignored and the current image will be kept while loading or downloading the new image.
  349. ///
  350. public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self {
  351. options.keepCurrentImageWhileLoading = enabled
  352. return self
  353. }
  354. /// Sets whether only the first frame from an animated image file should be loaded as a single image.
  355. /// - Parameter enabled: Whether the only the first frame should be loaded.
  356. /// - Returns: A ``KF/Builder`` with changes applied.
  357. ///
  358. /// Loading an animated images may take too much memory. It will be useful when you want to display a
  359. /// static preview of the first frame from an animated image.
  360. ///
  361. /// This option will be ignored if the target image is not animated image data.
  362. ///
  363. public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self {
  364. options.onlyLoadFirstFrame = enabled
  365. return self
  366. }
  367. /// Enables progressive image loading with a specified `ImageProgressive` setting to process the
  368. /// progressive JPEG data and display it in a progressive way.
  369. /// - Parameter progressive: The progressive settings which is used while loading.
  370. /// - Returns: A ``KF/Builder`` with changes applied.
  371. public func progressiveJPEG(_ progressive: ImageProgressive? = .init()) -> Self {
  372. options.progressiveJPEG = progressive
  373. return self
  374. }
  375. }
  376. // MARK: - Deprecated
  377. extension KF.Builder {
  378. /// Starts the loading process of `self` immediately.
  379. ///
  380. /// By default, a ``KFImage`` will not load its source until the `onAppear` is called. This is a lazily loading
  381. /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a
  382. /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once
  383. /// could help avoiding the flickering, with some performance trade-off.
  384. ///
  385. /// - Returns: The `Self` value with changes applied.
  386. @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.")
  387. public func loadImmediately(_ start: Bool = true) -> Self {
  388. return self
  389. }
  390. }
  391. // MARK: - Redirect Handler
  392. extension KF {
  393. /// Represents the detail information when a task redirect happens. It is wrapping necessary information for a
  394. /// ``ImageDownloadRedirectHandler``. See that protocol for more information.
  395. public struct RedirectPayload {
  396. /// The related session data task when the redirect happens. It is
  397. /// the current ``SessionDataTask`` which triggers this redirect.
  398. public let task: SessionDataTask
  399. /// The response received during redirection.
  400. public let response: HTTPURLResponse
  401. /// The request for redirection which can be modified.
  402. public let newRequest: URLRequest
  403. /// A closure for being called with modified request.
  404. public let completionHandler: (URLRequest?) -> Void
  405. }
  406. }