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.

419 lines
17 KiB

  1. //
  2. // ImageView+Kingfisher.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/6.
  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 !os(watchOS)
  27. #if os(macOS)
  28. import AppKit
  29. #else
  30. import UIKit
  31. #endif
  32. extension KingfisherWrapper where Base: KFCrossPlatformImageView {
  33. // MARK: Setting Image
  34. /// Sets an image to the image view with a `Source`.
  35. ///
  36. /// - Parameters:
  37. /// - source: The `Source` object defines data information from network or a data provider.
  38. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  39. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  40. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  41. /// `expectedContentLength`, this block will not be called.
  42. /// - completionHandler: Called when the image retrieved and set finished.
  43. /// - Returns: A task represents the image downloading.
  44. ///
  45. /// - Note:
  46. /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters
  47. /// have a default value except the `source`, you can set an image from a certain URL to an image view like this:
  48. ///
  49. /// ```
  50. /// // Set image from a network source.
  51. /// let url = URL(string: "https://example.com/image.png")!
  52. /// imageView.kf.setImage(with: .network(url))
  53. ///
  54. /// // Or set image from a data provider.
  55. /// let provider = LocalFileImageDataProvider(fileURL: fileURL)
  56. /// imageView.kf.setImage(with: .provider(provider))
  57. /// ```
  58. ///
  59. /// For both `.network` and `.provider` source, there are corresponding view extension methods. So the code
  60. /// above is equivalent to:
  61. ///
  62. /// ```
  63. /// imageView.kf.setImage(with: url)
  64. /// imageView.kf.setImage(with: provider)
  65. /// ```
  66. ///
  67. /// Internally, this method will use `KingfisherManager` to get the source.
  68. /// Since this method will perform UI changes, you must call it from the main thread.
  69. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  70. ///
  71. @discardableResult
  72. public func setImage(
  73. with source: Source?,
  74. placeholder: Placeholder? = nil,
  75. options: KingfisherOptionsInfo? = nil,
  76. progressBlock: DownloadProgressBlock? = nil,
  77. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  78. {
  79. var mutatingSelf = self
  80. guard let source = source else {
  81. mutatingSelf.placeholder = placeholder
  82. mutatingSelf.taskIdentifier = nil
  83. completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
  84. return nil
  85. }
  86. var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
  87. let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil
  88. if !options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet {
  89. // Always set placeholder while there is no image/placeholder yet.
  90. mutatingSelf.placeholder = placeholder
  91. }
  92. let maybeIndicator = indicator
  93. maybeIndicator?.startAnimatingView()
  94. let issuedIdentifier = Source.Identifier.next()
  95. mutatingSelf.taskIdentifier = issuedIdentifier
  96. if base.shouldPreloadAllAnimation() {
  97. options.preloadAllAnimationData = true
  98. }
  99. if let block = progressBlock {
  100. options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  101. }
  102. if let provider = ImageProgressiveProvider(options, refresh: { image in
  103. self.base.image = image
  104. }) {
  105. options.onDataReceived = (options.onDataReceived ?? []) + [provider]
  106. }
  107. options.onDataReceived?.forEach {
  108. $0.onShouldApply = { issuedIdentifier == self.taskIdentifier }
  109. }
  110. let task = KingfisherManager.shared.retrieveImage(
  111. with: source,
  112. options: options,
  113. downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
  114. completionHandler: { result in
  115. CallbackQueue.mainCurrentOrAsync.execute {
  116. maybeIndicator?.stopAnimatingView()
  117. guard issuedIdentifier == self.taskIdentifier else {
  118. let reason: KingfisherError.ImageSettingErrorReason
  119. do {
  120. let value = try result.get()
  121. reason = .notCurrentSourceTask(result: value, error: nil, source: source)
  122. } catch {
  123. reason = .notCurrentSourceTask(result: nil, error: error, source: source)
  124. }
  125. let error = KingfisherError.imageSettingError(reason: reason)
  126. completionHandler?(.failure(error))
  127. return
  128. }
  129. mutatingSelf.imageTask = nil
  130. mutatingSelf.taskIdentifier = nil
  131. switch result {
  132. case .success(let value):
  133. guard self.needsTransition(options: options, cacheType: value.cacheType) else {
  134. mutatingSelf.placeholder = nil
  135. self.base.image = value.image
  136. completionHandler?(result)
  137. return
  138. }
  139. self.makeTransition(image: value.image, transition: options.transition) {
  140. completionHandler?(result)
  141. }
  142. case .failure:
  143. if let image = options.onFailureImage {
  144. self.base.image = image
  145. }
  146. completionHandler?(result)
  147. }
  148. }
  149. }
  150. )
  151. mutatingSelf.imageTask = task
  152. return task
  153. }
  154. /// Sets an image to the image view with a requested resource.
  155. ///
  156. /// - Parameters:
  157. /// - resource: The `Resource` object contains information about the resource.
  158. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  159. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  160. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  161. /// `expectedContentLength`, this block will not be called.
  162. /// - completionHandler: Called when the image retrieved and set finished.
  163. /// - Returns: A task represents the image downloading.
  164. ///
  165. /// - Note:
  166. /// This is the easiest way to use Kingfisher to boost the image setting process from network. Since all parameters
  167. /// have a default value except the `resource`, you can set an image from a certain URL to an image view like this:
  168. ///
  169. /// ```
  170. /// let url = URL(string: "https://example.com/image.png")!
  171. /// imageView.kf.setImage(with: url)
  172. /// ```
  173. ///
  174. /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
  175. /// or network. Since this method will perform UI changes, you must call it from the main thread.
  176. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  177. ///
  178. @discardableResult
  179. public func setImage(
  180. with resource: Resource?,
  181. placeholder: Placeholder? = nil,
  182. options: KingfisherOptionsInfo? = nil,
  183. progressBlock: DownloadProgressBlock? = nil,
  184. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  185. {
  186. return setImage(
  187. with: resource?.convertToSource(),
  188. placeholder: placeholder,
  189. options: options,
  190. progressBlock: progressBlock,
  191. completionHandler: completionHandler)
  192. }
  193. /// Sets an image to the image view with a data provider.
  194. ///
  195. /// - Parameters:
  196. /// - provider: The `ImageDataProvider` object contains information about the data.
  197. /// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
  198. /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
  199. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
  200. /// `expectedContentLength`, this block will not be called.
  201. /// - completionHandler: Called when the image retrieved and set finished.
  202. /// - Returns: A task represents the image downloading.
  203. ///
  204. /// Internally, this method will use `KingfisherManager` to get the image data, from either cache
  205. /// or the data provider. Since this method will perform UI changes, you must call it from the main thread.
  206. /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
  207. ///
  208. @discardableResult
  209. public func setImage(
  210. with provider: ImageDataProvider?,
  211. placeholder: Placeholder? = nil,
  212. options: KingfisherOptionsInfo? = nil,
  213. progressBlock: DownloadProgressBlock? = nil,
  214. completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  215. {
  216. return setImage(
  217. with: provider.map { .provider($0) },
  218. placeholder: placeholder,
  219. options: options,
  220. progressBlock: progressBlock,
  221. completionHandler: completionHandler)
  222. }
  223. // MARK: Cancelling Downloading Task
  224. /// Cancels the image download task of the image view if it is running.
  225. /// Nothing will happen if the downloading has already finished.
  226. public func cancelDownloadTask() {
  227. imageTask?.cancel()
  228. }
  229. private func needsTransition(options: KingfisherParsedOptionsInfo, cacheType: CacheType) -> Bool {
  230. switch options.transition {
  231. case .none:
  232. return false
  233. #if !os(macOS)
  234. default:
  235. if options.forceTransition { return true }
  236. if cacheType == .none { return true }
  237. return false
  238. #endif
  239. }
  240. }
  241. private func makeTransition(image: KFCrossPlatformImage, transition: ImageTransition, done: @escaping () -> Void) {
  242. #if !os(macOS)
  243. // Force hiding the indicator without transition first.
  244. UIView.transition(
  245. with: self.base,
  246. duration: 0.0,
  247. options: [],
  248. animations: { self.indicator?.stopAnimatingView() },
  249. completion: { _ in
  250. var mutatingSelf = self
  251. mutatingSelf.placeholder = nil
  252. UIView.transition(
  253. with: self.base,
  254. duration: transition.duration,
  255. options: [transition.animationOptions, .allowUserInteraction],
  256. animations: { transition.animations?(self.base, image) },
  257. completion: { finished in
  258. transition.completion?(finished)
  259. done()
  260. }
  261. )
  262. }
  263. )
  264. #else
  265. done()
  266. #endif
  267. }
  268. }
  269. // MARK: - Associated Object
  270. private var taskIdentifierKey: Void?
  271. private var indicatorKey: Void?
  272. private var indicatorTypeKey: Void?
  273. private var placeholderKey: Void?
  274. private var imageTaskKey: Void?
  275. extension KingfisherWrapper where Base: KFCrossPlatformImageView {
  276. // MARK: Properties
  277. public private(set) var taskIdentifier: Source.Identifier.Value? {
  278. get {
  279. let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
  280. return box?.value
  281. }
  282. set {
  283. let box = newValue.map { Box($0) }
  284. setRetainedAssociatedObject(base, &taskIdentifierKey, box)
  285. }
  286. }
  287. /// Holds which indicator type is going to be used.
  288. /// Default is `.none`, means no indicator will be shown while downloading.
  289. public var indicatorType: IndicatorType {
  290. get {
  291. return getAssociatedObject(base, &indicatorTypeKey) ?? .none
  292. }
  293. set {
  294. switch newValue {
  295. case .none: indicator = nil
  296. case .activity: indicator = ActivityIndicator()
  297. case .image(let data): indicator = ImageIndicator(imageData: data)
  298. case .custom(let anIndicator): indicator = anIndicator
  299. }
  300. setRetainedAssociatedObject(base, &indicatorTypeKey, newValue)
  301. }
  302. }
  303. /// Holds any type that conforms to the protocol `Indicator`.
  304. /// The protocol `Indicator` has a `view` property that will be shown when loading an image.
  305. /// It will be `nil` if `indicatorType` is `.none`.
  306. public private(set) var indicator: Indicator? {
  307. get {
  308. let box: Box<Indicator>? = getAssociatedObject(base, &indicatorKey)
  309. return box?.value
  310. }
  311. set {
  312. // Remove previous
  313. if let previousIndicator = indicator {
  314. previousIndicator.view.removeFromSuperview()
  315. }
  316. // Add new
  317. if let newIndicator = newValue {
  318. // Set default indicator layout
  319. let view = newIndicator.view
  320. base.addSubview(view)
  321. view.translatesAutoresizingMaskIntoConstraints = false
  322. view.centerXAnchor.constraint(
  323. equalTo: base.centerXAnchor, constant: newIndicator.centerOffset.x).isActive = true
  324. view.centerYAnchor.constraint(
  325. equalTo: base.centerYAnchor, constant: newIndicator.centerOffset.y).isActive = true
  326. switch newIndicator.sizeStrategy(in: base) {
  327. case .intrinsicSize:
  328. break
  329. case .full:
  330. view.heightAnchor.constraint(equalTo: base.heightAnchor, constant: 0).isActive = true
  331. view.widthAnchor.constraint(equalTo: base.widthAnchor, constant: 0).isActive = true
  332. case .size(let size):
  333. view.heightAnchor.constraint(equalToConstant: size.height).isActive = true
  334. view.widthAnchor.constraint(equalToConstant: size.width).isActive = true
  335. }
  336. newIndicator.view.isHidden = true
  337. }
  338. // Save in associated object
  339. // Wrap newValue with Box to workaround an issue that Swift does not recognize
  340. // and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872
  341. setRetainedAssociatedObject(base, &indicatorKey, newValue.map(Box.init))
  342. }
  343. }
  344. private var imageTask: DownloadTask? {
  345. get { return getAssociatedObject(base, &imageTaskKey) }
  346. set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
  347. }
  348. /// Represents the `Placeholder` used for this image view. A `Placeholder` will be shown in the view while
  349. /// it is downloading an image.
  350. public private(set) var placeholder: Placeholder? {
  351. get { return getAssociatedObject(base, &placeholderKey) }
  352. set {
  353. if let previousPlaceholder = placeholder {
  354. previousPlaceholder.remove(from: base)
  355. }
  356. if let newPlaceholder = newValue {
  357. newPlaceholder.add(to: base)
  358. } else {
  359. base.image = nil
  360. }
  361. setRetainedAssociatedObject(base, &placeholderKey, newValue)
  362. }
  363. }
  364. }
  365. @objc extension KFCrossPlatformImageView {
  366. func shouldPreloadAllAnimation() -> Bool { return true }
  367. }
  368. extension KingfisherWrapper where Base: KFCrossPlatformImageView {
  369. /// Gets the image URL bound to this image view.
  370. @available(*, deprecated, message: "Use `taskIdentifier` instead to identify a setting task.")
  371. public private(set) var webURL: URL? {
  372. get { return nil }
  373. set { }
  374. }
  375. }
  376. #endif