Indicator.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. //
  2. // Indicator.swift
  3. // Kingfisher
  4. //
  5. // Created by João D. Moreira on 30/08/16.
  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 canImport(AppKit) && !targetEnvironment(macCatalyst)
  28. import AppKit
  29. public typealias IndicatorView = NSView
  30. #else
  31. import UIKit
  32. public typealias IndicatorView = UIView
  33. #endif
  34. /// Represents the activity indicator type that should be added to an image view when an image is being downloaded.
  35. public enum IndicatorType {
  36. /// No indicator.
  37. case none
  38. /// Uses the system activity indicator.
  39. case activity
  40. /// Uses an image as an indicator. GIF is supported.
  41. case image(imageData: Data)
  42. /// Uses a custom indicator.
  43. ///
  44. /// The type of the associated value should conform to the ``Indicator`` protocol.
  45. case custom(indicator: Indicator)
  46. }
  47. /// An indicator type which can be used to show that the download task is in progress.
  48. public protocol Indicator {
  49. /// Called when the indicator should start animating.
  50. func startAnimatingView()
  51. /// Called when the indicator should stop animating.
  52. func stopAnimatingView()
  53. /// Center offset of the indicator.
  54. ///
  55. /// Kingfisher will use this value to determine the position of the indicator in the superview.
  56. var centerOffset: CGPoint { get }
  57. /// The indicator view which would be added to the superview.
  58. var view: IndicatorView { get }
  59. /// The size strategy used when adding the indicator to the image view.
  60. /// - Parameter imageView: The superview of the indicator.
  61. /// - Returns: An ``IndicatorSizeStrategy`` that determines how the indicator should be sized.
  62. func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy
  63. }
  64. /// The idicator size strategy used when sizing the indicator in the image view.
  65. public enum IndicatorSizeStrategy {
  66. /// Uses the intrinsic size of the indicator.
  67. case intrinsicSize
  68. /// Match the size of the super view of the indicator.
  69. case full
  70. /// Uses the associated `CGSize` to set the indicator size.
  71. case size(CGSize)
  72. }
  73. extension Indicator {
  74. /// Default implementation of ``Indicator/centerOffset-7jxdw`` of the ``Indicator``.
  75. ///
  76. /// The default value is `.zero`, which means that there is no offset for the indicator view.
  77. public var centerOffset: CGPoint {
  78. .zero
  79. }
  80. /// Default implementation of ``Indicator/sizeStrategy(in:)-5x0b4`` of the ``Indicator``.
  81. ///
  82. /// The default value is ``IndicatorSizeStrategy/full``, means that the indicator will pin to the same height and
  83. /// width as the image view.
  84. /// - Parameter imageView: The image view which holds the indicator.
  85. /// - Returns: The desired ``IndicatorSizeStrategy``
  86. public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy {
  87. .full
  88. }
  89. }
  90. // Displays a NSProgressIndicator / UIActivityIndicatorView
  91. final class ActivityIndicator: Indicator {
  92. #if os(macOS)
  93. private let activityIndicatorView: NSProgressIndicator
  94. #else
  95. private let activityIndicatorView: UIActivityIndicatorView
  96. #endif
  97. private var animatingCount = 0
  98. var view: IndicatorView {
  99. return activityIndicatorView
  100. }
  101. func startAnimatingView() {
  102. if animatingCount == 0 {
  103. #if os(macOS)
  104. activityIndicatorView.startAnimation(nil)
  105. #else
  106. activityIndicatorView.startAnimating()
  107. #endif
  108. activityIndicatorView.isHidden = false
  109. }
  110. animatingCount += 1
  111. }
  112. func stopAnimatingView() {
  113. animatingCount = max(animatingCount - 1, 0)
  114. if animatingCount == 0 {
  115. #if os(macOS)
  116. activityIndicatorView.stopAnimation(nil)
  117. #else
  118. activityIndicatorView.stopAnimating()
  119. #endif
  120. activityIndicatorView.isHidden = true
  121. }
  122. }
  123. func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy {
  124. return .intrinsicSize
  125. }
  126. init() {
  127. #if os(macOS)
  128. activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16))
  129. activityIndicatorView.controlSize = .small
  130. activityIndicatorView.style = .spinning
  131. #else
  132. let indicatorStyle: UIActivityIndicatorView.Style
  133. #if os(tvOS)
  134. if #available(tvOS 13.0, *) {
  135. indicatorStyle = UIActivityIndicatorView.Style.large
  136. } else {
  137. indicatorStyle = UIActivityIndicatorView.Style.white
  138. }
  139. #elseif os(visionOS)
  140. indicatorStyle = UIActivityIndicatorView.Style.medium
  141. #else
  142. if #available(iOS 13.0, * ) {
  143. indicatorStyle = UIActivityIndicatorView.Style.medium
  144. } else {
  145. indicatorStyle = UIActivityIndicatorView.Style.gray
  146. }
  147. #endif
  148. activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle)
  149. #endif
  150. }
  151. }
  152. #if canImport(UIKit)
  153. extension UIActivityIndicatorView.Style {
  154. #if compiler(>=5.1)
  155. #else
  156. static let large = UIActivityIndicatorView.Style.white
  157. #if !os(tvOS)
  158. static let medium = UIActivityIndicatorView.Style.gray
  159. #endif
  160. #endif
  161. }
  162. #endif
  163. // MARK: - ImageIndicator
  164. // Displays an ImageView. Supports gif
  165. final class ImageIndicator: Indicator {
  166. private let animatedImageIndicatorView: KFCrossPlatformImageView
  167. var view: IndicatorView {
  168. return animatedImageIndicatorView
  169. }
  170. init?(
  171. imageData data: Data,
  172. processor: ImageProcessor = DefaultImageProcessor.default,
  173. options: KingfisherParsedOptionsInfo? = nil)
  174. {
  175. var options = options ?? KingfisherParsedOptionsInfo(nil)
  176. // Use normal image view to show animations, so we need to preload all animation data.
  177. if !options.preloadAllAnimationData {
  178. options.preloadAllAnimationData = true
  179. }
  180. guard let image = processor.process(item: .data(data), options: options) else {
  181. return nil
  182. }
  183. animatedImageIndicatorView = KFCrossPlatformImageView()
  184. animatedImageIndicatorView.image = image
  185. #if os(macOS)
  186. // Need for gif to animate on macOS
  187. animatedImageIndicatorView.imageScaling = .scaleNone
  188. animatedImageIndicatorView.canDrawSubviewsIntoLayer = true
  189. #else
  190. animatedImageIndicatorView.contentMode = .center
  191. #endif
  192. }
  193. func startAnimatingView() {
  194. #if os(macOS)
  195. animatedImageIndicatorView.animates = true
  196. #else
  197. animatedImageIndicatorView.startAnimating()
  198. #endif
  199. animatedImageIndicatorView.isHidden = false
  200. }
  201. func stopAnimatingView() {
  202. #if os(macOS)
  203. animatedImageIndicatorView.animates = false
  204. #else
  205. animatedImageIndicatorView.stopAnimating()
  206. #endif
  207. animatedImageIndicatorView.isHidden = true
  208. }
  209. }
  210. #endif