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.

602 lines
23 KiB

2 years ago
  1. // The MIT License (MIT)
  2. //
  3. // Copyright (c) 2015-2021 Alexander Grebenyuk (github.com/kean).
  4. import Foundation
  5. #if !os(macOS)
  6. import UIKit.UIImage
  7. import UIKit.UIColor
  8. /// Alias for `UIImage`.
  9. public typealias PlatformImage = UIImage
  10. #else
  11. import AppKit.NSImage
  12. /// Alias for `NSImage`.
  13. public typealias PlatformImage = NSImage
  14. #endif
  15. /// Displays images. Add the conformance to this protocol to your views to make
  16. /// them compatible with Nuke image loading extensions.
  17. ///
  18. /// The protocol is defined as `@objc` to make it possible to override its
  19. /// methods in extensions (e.g. you can override `nuke_display(image:)` in
  20. /// `UIImageView` subclass like `Gifu.ImageView).
  21. ///
  22. /// The protocol and its methods have prefixes to make sure they don't clash
  23. /// with other similar methods and protocol in Objective-C runtime.
  24. @objc public protocol Nuke_ImageDisplaying {
  25. /// Display a given image.
  26. @objc func nuke_display(image: PlatformImage?)
  27. #if os(macOS)
  28. @objc var layer: CALayer? { get }
  29. #endif
  30. }
  31. #if os(macOS)
  32. public extension Nuke_ImageDisplaying {
  33. var layer: CALayer? { nil }
  34. }
  35. #endif
  36. #if os(iOS) || os(tvOS)
  37. import UIKit
  38. /// A `UIView` that implements `ImageDisplaying` protocol.
  39. public typealias ImageDisplayingView = UIView & Nuke_ImageDisplaying
  40. extension UIImageView: Nuke_ImageDisplaying {
  41. /// Displays an image.
  42. open func nuke_display(image: UIImage?) {
  43. self.image = image
  44. }
  45. }
  46. #elseif os(macOS)
  47. import Cocoa
  48. /// An `NSObject` that implements `ImageDisplaying` and `Animating` protocols.
  49. /// Can support `NSView` and `NSCell`. The latter can return nil for layer.
  50. public typealias ImageDisplayingView = NSObject & Nuke_ImageDisplaying
  51. extension NSImageView: Nuke_ImageDisplaying {
  52. /// Displays an image.
  53. open func nuke_display(image: NSImage?) {
  54. self.image = image
  55. }
  56. }
  57. #elseif os(watchOS)
  58. import WatchKit
  59. /// A `WKInterfaceObject` that implements `ImageDisplaying` protocol.
  60. public typealias ImageDisplayingView = WKInterfaceObject & Nuke_ImageDisplaying
  61. extension WKInterfaceImage: Nuke_ImageDisplaying {
  62. /// Displays an image.
  63. open func nuke_display(image: UIImage?) {
  64. self.setImage(image)
  65. }
  66. }
  67. #endif
  68. // MARK: - ImageView Extensions
  69. @discardableResult
  70. public func loadImage(with request: ImageRequestConvertible,
  71. options: ImageLoadingOptions = ImageLoadingOptions.shared,
  72. into view: ImageDisplayingView,
  73. completion: @escaping (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void) -> ImageTask? {
  74. loadImage(with: request, options: options, into: view, progress: nil, completion: completion)
  75. }
  76. /// Loads an image with the given request and displays it in the view.
  77. ///
  78. /// Before loading a new image, the view is prepared for reuse by canceling any
  79. /// outstanding requests and removing a previously displayed image.
  80. ///
  81. /// If the image is stored in the memory cache, it is displayed immediately with
  82. /// no animations. If not, the image is loaded using an image pipeline. When the
  83. /// image is loading, the `placeholder` is displayed. When the request
  84. /// completes the loaded image is displayed (or `failureImage` in case of an error)
  85. /// with the selected animation.
  86. ///
  87. /// - parameter options: `ImageLoadingOptions.shared` by default.
  88. /// - parameter view: Nuke keeps a weak reference to the view. If the view is deallocated
  89. /// the associated request automatically gets canceled.
  90. /// - parameter progress: A closure to be called periodically on the main thread
  91. /// when the progress is updated. `nil` by default.
  92. /// - parameter completion: A closure to be called on the main thread when the
  93. /// request is finished. Gets called synchronously if the response was found in
  94. /// the memory cache. `nil` by default.
  95. /// - returns: An image task or `nil` if the image was found in the memory cache.
  96. @discardableResult
  97. public func loadImage(with request: ImageRequestConvertible,
  98. options: ImageLoadingOptions = ImageLoadingOptions.shared,
  99. into view: ImageDisplayingView,
  100. progress: ((_ intermediateResponse: ImageResponse?, _ completedUnitCount: Int64, _ totalUnitCount: Int64) -> Void)? = nil,
  101. completion: ((_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil) -> ImageTask? {
  102. assert(Thread.isMainThread)
  103. let controller = ImageViewController.controller(for: view)
  104. return controller.loadImage(with: request.asImageRequest(), options: options, progress: progress, completion: completion)
  105. }
  106. /// Cancels an outstanding request associated with the view.
  107. public func cancelRequest(for view: ImageDisplayingView) {
  108. assert(Thread.isMainThread)
  109. ImageViewController.controller(for: view).cancelOutstandingTask()
  110. }
  111. // MARK: - ImageLoadingOptions
  112. /// A range of options that control how the image is loaded and displayed.
  113. public struct ImageLoadingOptions {
  114. /// Shared options.
  115. public static var shared = ImageLoadingOptions()
  116. /// Placeholder to be displayed when the image is loading. `nil` by default.
  117. public var placeholder: PlatformImage?
  118. /// Image to be displayed when the request fails. `nil` by default.
  119. public var failureImage: PlatformImage?
  120. #if os(iOS) || os(tvOS) || os(macOS)
  121. /// The image transition animation performed when displaying a loaded image.
  122. /// Only runs when the image was not found in memory cache. `nil` by default.
  123. public var transition: Transition?
  124. /// The image transition animation performed when displaying a failure image.
  125. /// `nil` by default.
  126. public var failureImageTransition: Transition?
  127. /// If true, the requested image will always appear with transition, even
  128. /// when loaded from cache.
  129. public var alwaysTransition = false
  130. #endif
  131. /// If true, every time you request a new image for a view, the view will be
  132. /// automatically prepared for reuse: image will be set to `nil`, and animations
  133. /// will be removed. `true` by default.
  134. public var isPrepareForReuseEnabled = true
  135. /// If `true`, every progressively generated preview produced by the pipeline
  136. /// is going to be displayed. `true` by default.
  137. ///
  138. /// - note: To enable progressive decoding, see `ImagePipeline.Configuration`,
  139. /// `isProgressiveDecodingEnabled` option.
  140. public var isProgressiveRenderingEnabled = true
  141. /// Custom pipeline to be used. `nil` by default.
  142. public var pipeline: ImagePipeline?
  143. #if os(iOS) || os(tvOS)
  144. /// Content modes to be used for each image type (placeholder, success,
  145. /// failure). `nil` by default (don't change content mode).
  146. public var contentModes: ContentModes?
  147. /// Custom content modes to be used for each image type (placeholder, success,
  148. /// failure).
  149. public struct ContentModes {
  150. /// Content mode to be used for the loaded image.
  151. public var success: UIView.ContentMode
  152. /// Content mode to be used when displaying a `failureImage`.
  153. public var failure: UIView.ContentMode
  154. /// Content mode to be used when displaying a `placeholder`.
  155. public var placeholder: UIView.ContentMode
  156. /// - parameter success: A content mode to be used with a loaded image.
  157. /// - parameter failure: A content mode to be used with a `failureImage`.
  158. /// - parameter placeholder: A content mode to be used with a `placeholder`.
  159. public init(success: UIView.ContentMode, failure: UIView.ContentMode, placeholder: UIView.ContentMode) {
  160. self.success = success; self.failure = failure; self.placeholder = placeholder
  161. }
  162. }
  163. /// Tint colors to be used for each image type (placeholder, success,
  164. /// failure). `nil` by default (don't change tint color or rendering mode).
  165. public var tintColors: TintColors?
  166. /// Custom tint color to be used for each image type (placeholder, success,
  167. /// failure).
  168. public struct TintColors {
  169. /// Tint color to be used for the loaded image.
  170. public var success: UIColor?
  171. /// Tint color to be used when displaying a `failureImage`.
  172. public var failure: UIColor?
  173. /// Tint color to be used when displaying a `placeholder`.
  174. public var placeholder: UIColor?
  175. /// - parameter success: A tint color to be used with a loaded image.
  176. /// - parameter failure: A tint color to be used with a `failureImage`.
  177. /// - parameter placeholder: A tint color to be used with a `placeholder`.
  178. public init(success: UIColor?, failure: UIColor?, placeholder: UIColor?) {
  179. self.success = success; self.failure = failure; self.placeholder = placeholder
  180. }
  181. }
  182. #endif
  183. #if os(iOS) || os(tvOS)
  184. /// - parameter placeholder: Placeholder to be displayed when the image is
  185. /// loading . `nil` by default.
  186. /// - parameter transition: The image transition animation performed when
  187. /// displaying a loaded image. Only runs when the image was not found in
  188. /// memory cache. `nil` by default (no animations).
  189. /// - parameter failureImage: Image to be displayd when request fails.
  190. /// `nil` by default.
  191. /// - parameter failureImageTransition: The image transition animation
  192. /// performed when displaying a failure image. `nil` by default.
  193. /// - parameter contentModes: Content modes to be used for each image type
  194. /// (placeholder, success, failure). `nil` by default (don't change content mode).
  195. public init(placeholder: UIImage? = nil, transition: Transition? = nil, failureImage: UIImage? = nil, failureImageTransition: Transition? = nil, contentModes: ContentModes? = nil, tintColors: TintColors? = nil) {
  196. self.placeholder = placeholder
  197. self.transition = transition
  198. self.failureImage = failureImage
  199. self.failureImageTransition = failureImageTransition
  200. self.contentModes = contentModes
  201. self.tintColors = tintColors
  202. }
  203. #elseif os(macOS)
  204. public init(placeholder: NSImage? = nil, transition: Transition? = nil, failureImage: NSImage? = nil, failureImageTransition: Transition? = nil) {
  205. self.placeholder = placeholder
  206. self.transition = transition
  207. self.failureImage = failureImage
  208. self.failureImageTransition = failureImageTransition
  209. }
  210. #elseif os(watchOS)
  211. public init(placeholder: UIImage? = nil, failureImage: UIImage? = nil) {
  212. self.placeholder = placeholder
  213. self.failureImage = failureImage
  214. }
  215. #endif
  216. #if os(iOS) || os(tvOS)
  217. /// An animated image transition.
  218. public struct Transition {
  219. var style: Style
  220. enum Style { // internal representation
  221. case fadeIn(parameters: Parameters)
  222. case custom((ImageDisplayingView, UIImage) -> Void)
  223. }
  224. struct Parameters { // internal representation
  225. let duration: TimeInterval
  226. let options: UIView.AnimationOptions
  227. }
  228. /// Fade-in transition (cross-fade in case the image view is already
  229. /// displaying an image).
  230. public static func fadeIn(duration: TimeInterval, options: UIView.AnimationOptions = .allowUserInteraction) -> Transition {
  231. Transition(style: .fadeIn(parameters: Parameters(duration: duration, options: options)))
  232. }
  233. /// Custom transition. Only runs when the image was not found in memory cache.
  234. public static func custom(_ closure: @escaping (ImageDisplayingView, UIImage) -> Void) -> Transition {
  235. Transition(style: .custom(closure))
  236. }
  237. }
  238. #elseif os(macOS)
  239. /// An animated image transition.
  240. public struct Transition {
  241. var style: Style
  242. enum Style { // internal representation
  243. case fadeIn(parameters: Parameters)
  244. case custom((ImageDisplayingView, NSImage) -> Void)
  245. }
  246. struct Parameters { // internal representation
  247. let duration: TimeInterval
  248. }
  249. /// Fade-in transition.
  250. public static func fadeIn(duration: TimeInterval) -> Transition {
  251. Transition(style: .fadeIn(parameters: Parameters(duration: duration)))
  252. }
  253. /// Custom transition. Only runs when the image was not found in memory cache.
  254. public static func custom(_ closure: @escaping (ImageDisplayingView, NSImage) -> Void) -> Transition {
  255. Transition(style: .custom(closure))
  256. }
  257. }
  258. #endif
  259. public init() {}
  260. }
  261. // MARK: - ImageViewController
  262. /// Manages image requests on behalf of an image view.
  263. ///
  264. /// - note: With a few modifications this might become public at some point,
  265. /// however as it stands today `ImageViewController` is just a helper class,
  266. /// making it public wouldn't expose any additional functionality to the users.
  267. private final class ImageViewController {
  268. private weak var imageView: ImageDisplayingView?
  269. private var task: ImageTask?
  270. // Automatically cancel the request when the view is deallocated.
  271. deinit {
  272. cancelOutstandingTask()
  273. }
  274. init(view: /* weak */ ImageDisplayingView) {
  275. self.imageView = view
  276. }
  277. // MARK: - Associating Controller
  278. static var controllerAK = "ImageViewController.AssociatedKey"
  279. // Lazily create a controller for a given view and associate it with a view.
  280. static func controller(for view: ImageDisplayingView) -> ImageViewController {
  281. if let controller = objc_getAssociatedObject(view, &ImageViewController.controllerAK) as? ImageViewController {
  282. return controller
  283. }
  284. let controller = ImageViewController(view: view)
  285. objc_setAssociatedObject(view, &ImageViewController.controllerAK, controller, .OBJC_ASSOCIATION_RETAIN)
  286. return controller
  287. }
  288. // MARK: - Loading Images
  289. func loadImage(with request: ImageRequest,
  290. options: ImageLoadingOptions,
  291. progress progressHandler: ((_ intermediateResponse: ImageResponse?, _ completedUnitCount: Int64, _ totalUnitCount: Int64) -> Void)? = nil,
  292. completion: ((_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil) -> ImageTask? {
  293. cancelOutstandingTask()
  294. guard let imageView = imageView else {
  295. return nil
  296. }
  297. if options.isPrepareForReuseEnabled { // enabled by default
  298. #if os(iOS) || os(tvOS)
  299. imageView.layer.removeAllAnimations()
  300. #elseif os(macOS)
  301. let layer = (imageView as? NSView)?.layer ?? imageView.layer
  302. layer?.removeAllAnimations()
  303. #endif
  304. }
  305. let pipeline = options.pipeline ?? ImagePipeline.shared
  306. // Quick synchronous memory cache lookup
  307. if let image = pipeline.cachedImage(for: request) {
  308. let response = ImageResponse(container: image)
  309. handle(result: .success(response), fromMemCache: true, options: options)
  310. if !image.isPreview { // Final image was downloaded
  311. completion?(.success(response))
  312. return nil // No task to perform
  313. }
  314. }
  315. // Display a placeholder.
  316. if var placeholder = options.placeholder {
  317. #if os(iOS) || os(tvOS)
  318. if let tintColor = options.tintColors?.placeholder {
  319. placeholder = placeholder.withRenderingMode(.alwaysTemplate)
  320. imageView.tintColor = tintColor
  321. }
  322. if let contentMode = options.contentModes?.placeholder {
  323. imageView.contentMode = contentMode
  324. }
  325. #endif
  326. imageView.nuke_display(image: placeholder)
  327. } else if options.isPrepareForReuseEnabled {
  328. imageView.nuke_display(image: nil) // Remove previously displayed images (if any)
  329. }
  330. task = pipeline.loadImage(with: request, queue: .main, progress: { [weak self] response, completedCount, totalCount in
  331. if let response = response, options.isProgressiveRenderingEnabled {
  332. self?.handle(partialImage: response, options: options)
  333. }
  334. progressHandler?(response, completedCount, totalCount)
  335. }, completion: { [weak self] result in
  336. self?.handle(result: result, fromMemCache: false, options: options)
  337. completion?(result)
  338. })
  339. return task
  340. }
  341. func cancelOutstandingTask() {
  342. task?.cancel() // The pipeline guarantees no callbacks to be deliver after cancellation
  343. task = nil
  344. }
  345. // MARK: - Handling Responses
  346. #if os(iOS) || os(tvOS)
  347. private func handle(result: Result<ImageResponse, ImagePipeline.Error>, fromMemCache: Bool, options: ImageLoadingOptions) {
  348. switch result {
  349. case let .success(response):
  350. display(response.image, options.transition, options.alwaysTransition, fromMemCache, options.contentModes?.success, options.tintColors?.success)
  351. case .failure:
  352. if let failureImage = options.failureImage {
  353. display(failureImage, options.failureImageTransition, options.alwaysTransition, fromMemCache, options.contentModes?.failure, options.tintColors?.failure)
  354. }
  355. }
  356. self.task = nil
  357. }
  358. private func handle(partialImage response: ImageResponse, options: ImageLoadingOptions) {
  359. display(response.image, options.transition, options.alwaysTransition, false, options.contentModes?.success, options.tintColors?.success)
  360. }
  361. // swiftlint:disable:next function_parameter_count
  362. private func display(_ image: UIImage, _ transition: ImageLoadingOptions.Transition?, _ alwaysTransition: Bool, _ fromMemCache: Bool, _ newContentMode: UIView.ContentMode?, _ newTintColor: UIColor?) {
  363. guard let imageView = imageView else {
  364. return
  365. }
  366. var image = image
  367. if let newTintColor = newTintColor {
  368. image = image.withRenderingMode(.alwaysTemplate)
  369. imageView.tintColor = newTintColor
  370. }
  371. if !fromMemCache || alwaysTransition, let transition = transition {
  372. switch transition.style {
  373. case let .fadeIn(params):
  374. runFadeInTransition(image: image, params: params, contentMode: newContentMode)
  375. case let .custom(closure):
  376. // The user is reponsible for both displaying an image and performing
  377. // animations.
  378. closure(imageView, image)
  379. }
  380. } else {
  381. imageView.nuke_display(image: image)
  382. }
  383. if let newContentMode = newContentMode {
  384. imageView.contentMode = newContentMode
  385. }
  386. }
  387. // Image view used for cross-fade transition between images with different
  388. // content modes.
  389. private lazy var transitionImageView = UIImageView()
  390. private func runFadeInTransition(image: UIImage, params: ImageLoadingOptions.Transition.Parameters, contentMode: UIView.ContentMode?) {
  391. guard let imageView = imageView else {
  392. return
  393. }
  394. // Special case where it animates between content modes, only works
  395. // on imageView subclasses.
  396. if let contentMode = contentMode, imageView.contentMode != contentMode, let imageView = imageView as? UIImageView, imageView.image != nil {
  397. runCrossDissolveWithContentMode(imageView: imageView, image: image, params: params)
  398. } else {
  399. runSimpleFadeIn(image: image, params: params)
  400. }
  401. }
  402. private func runSimpleFadeIn(image: UIImage, params: ImageLoadingOptions.Transition.Parameters) {
  403. guard let imageView = imageView else {
  404. return
  405. }
  406. UIView.transition(
  407. with: imageView,
  408. duration: params.duration,
  409. options: params.options.union(.transitionCrossDissolve),
  410. animations: {
  411. imageView.nuke_display(image: image)
  412. },
  413. completion: nil
  414. )
  415. }
  416. /// Performs cross-dissolve animation alonside transition to a new content
  417. /// mode. This isn't natively supported feature and it requires a second
  418. /// image view. There might be better ways to implement it.
  419. private func runCrossDissolveWithContentMode(imageView: UIImageView, image: UIImage, params: ImageLoadingOptions.Transition.Parameters) {
  420. // Lazily create a transition view.
  421. let transitionView = self.transitionImageView
  422. // Create a transition view which mimics current view's contents.
  423. transitionView.image = imageView.image
  424. transitionView.contentMode = imageView.contentMode
  425. imageView.addSubview(transitionView)
  426. transitionView.frame = imageView.bounds
  427. // "Manual" cross-fade.
  428. transitionView.alpha = 1
  429. imageView.alpha = 0
  430. imageView.image = image // Display new image in current view
  431. UIView.animate(
  432. withDuration: params.duration,
  433. delay: 0,
  434. options: params.options,
  435. animations: {
  436. transitionView.alpha = 0
  437. imageView.alpha = 1
  438. },
  439. completion: { isCompleted in
  440. if isCompleted {
  441. transitionView.removeFromSuperview()
  442. }
  443. }
  444. )
  445. }
  446. #elseif os(macOS)
  447. private func handle(result: Result<ImageResponse, ImagePipeline.Error>, fromMemCache: Bool, options: ImageLoadingOptions) {
  448. // NSImageView doesn't support content mode, unfortunately.
  449. switch result {
  450. case let .success(response):
  451. display(response.image, options.transition, options.alwaysTransition, fromMemCache)
  452. case .failure:
  453. if let failureImage = options.failureImage {
  454. display(failureImage, options.failureImageTransition, options.alwaysTransition, fromMemCache)
  455. }
  456. }
  457. self.task = nil
  458. }
  459. private func handle(partialImage response: ImageResponse, options: ImageLoadingOptions) {
  460. display(response.image, options.transition, options.alwaysTransition, false)
  461. }
  462. private func display(_ image: NSImage, _ transition: ImageLoadingOptions.Transition?, _ alwaysTransition: Bool, _ fromMemCache: Bool) {
  463. guard let imageView = imageView else {
  464. return
  465. }
  466. if !fromMemCache || alwaysTransition, let transition = transition {
  467. switch transition.style {
  468. case let .fadeIn(params):
  469. runFadeInTransition(image: image, params: params)
  470. case let .custom(closure):
  471. // The user is reponsible for both displaying an image and performing
  472. // animations.
  473. closure(imageView, image)
  474. }
  475. } else {
  476. imageView.nuke_display(image: image)
  477. }
  478. }
  479. private func runFadeInTransition(image: NSImage, params: ImageLoadingOptions.Transition.Parameters) {
  480. let animation = CABasicAnimation(keyPath: "opacity")
  481. animation.duration = params.duration
  482. animation.fromValue = 0
  483. animation.toValue = 1
  484. imageView?.layer?.add(animation, forKey: "imageTransition")
  485. imageView?.nuke_display(image: image)
  486. }
  487. #elseif os(watchOS)
  488. private func handle(result: Result<ImageResponse, ImagePipeline.Error>, fromMemCache: Bool, options: ImageLoadingOptions) {
  489. switch result {
  490. case let .success(response):
  491. imageView?.nuke_display(image: response.image)
  492. case .failure:
  493. if let failureImage = options.failureImage {
  494. imageView?.nuke_display(image: failureImage)
  495. }
  496. }
  497. self.task = nil
  498. }
  499. private func handle(partialImage response: ImageResponse, options: ImageLoadingOptions) {
  500. imageView?.nuke_display(image: response.image)
  501. }
  502. #endif
  503. }