<NME> ImageViewExtensions.swift
<BEF> // The MIT License (MIT)
//
// Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).

import Foundation

#if !os(macOS)
import UIKit.UIImage
import UIKit.UIColor
#else
import AppKit.NSImage
#endif

#if os(iOS) || os(tvOS) || os(macOS)

/// Displays images. Add the conformance to this protocol to your views to make
/// them compatible with Nuke image loading extensions.
///
/// The protocol is defined as `@objc` to make it possible to override its
/// methods in extensions (e.g. you can override `nuke_display(image:data:)` in
/// `UIImageView` subclass like `Gifu.ImageView).
///
/// The protocol and its methods have prefixes to make sure they don't clash
/// with other similar methods and protocol in Objective-C runtime.
@MainActor
@objc public protocol Nuke_ImageDisplaying {
    /// Display a given image.
    @objc func nuke_display(image: PlatformImage?, data: Data?)

    #if os(macOS)
    @objc var layer: CALayer? { get }
    #endif
}

extension Nuke_ImageDisplaying {
    func display(_ container: ImageContainer) {
        nuke_display(image: container.image, data: container.data)
    }
}

#if os(macOS)
extension Nuke_ImageDisplaying {
    public var layer: CALayer? { nil }
}
#endif

#if os(iOS) || os(tvOS)
import UIKit
/// A `UIView` that implements `ImageDisplaying` protocol.
public typealias ImageDisplayingView = UIView & Nuke_ImageDisplaying

extension UIImageView: Nuke_ImageDisplaying {
    /// Displays an image.
    open func nuke_display(image: UIImage?, data: Data? = nil) {
        self.image = image
    }
}
#elseif os(macOS)
import Cocoa
/// An `NSObject` that implements `ImageDisplaying`  and `Animating` protocols.
/// Can support `NSView` and `NSCell`. The latter can return nil for layer.
public typealias ImageDisplayingView = NSObject & Nuke_ImageDisplaying

extension NSImageView: Nuke_ImageDisplaying {
    /// Displays an image.
    open func nuke_display(image: NSImage?, data: Data? = nil) {
        self.image = image
    }
}
#endif

#if os(tvOS)
import TVUIKit

extension TVPosterView: Nuke_ImageDisplaying {
    /// Displays an image.
    open func nuke_display(image: UIImage?, data: Data? = nil) {
        self.image = image
    }
}
#endif

// MARK: - ImageView Extensions

/// Loads an image with the given request and displays it in the view.
///
/// See the complete method signature for more information.
@MainActor
@discardableResult public func loadImage(
    with request: (any ImageRequestConvertible)?,
    options: ImageLoadingOptions = ImageLoadingOptions.shared,
    into view: ImageDisplayingView,
    completion: @escaping (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void
) -> ImageTask? {
    loadImage(with: request, options: options, into: view, progress: nil, completion: completion)
}

/// Loads an image with the given request and displays it in the view.
///
/// Before loading a new image, the view is prepared for reuse by canceling any
/// outstanding requests and removing a previously displayed image.
///
/// If the image is stored in the memory cache, it is displayed immediately with
/// no animations. If not, the image is loaded using an image pipeline. When the
/// image is loading, the `placeholder` is displayed. When the request
/// completes the loaded image is displayed (or `failureImage` in case of an error)
/// with the selected animation.
///
/// - parameters:
///   - request: The image request. If `nil`, it's handled as a failure scenario.
///   - options: `ImageLoadingOptions.shared` by default.
///   - view: Nuke keeps a weak reference to the view. If the view is deallocated
///   the associated request automatically gets canceled.
///   - progress: A closure to be called periodically on the main thread
///   when the progress is updated.
///   - completion: A closure to be called on the main thread when the
///   request is finished. Gets called synchronously if the response was found in
///   the memory cache.
///
/// - returns: An image task or `nil` if the image was found in the memory cache.
@MainActor
@discardableResult public func loadImage(
    with request: (any ImageRequestConvertible)?,
    options: ImageLoadingOptions = ImageLoadingOptions.shared,
    into view: ImageDisplayingView,
    progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil,
    completion: ((_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil
) -> ImageTask? {
    let controller = ImageViewController.controller(for: view)
    return controller.loadImage(with: request?.asImageRequest(), options: options, progress: progress, completion: completion)
}

/// Cancels an outstanding request associated with the view.
@MainActor
public func cancelRequest(for view: ImageDisplayingView) {
    ImageViewController.controller(for: view).cancelOutstandingTask()
}

// MARK: - ImageViewController

/// Manages image requests on behalf of an image view.
///
/// - note: With a few modifications this might become public at some point,
/// however as it stands today `ImageViewController` is just a helper class,
/// making it public wouldn't expose any additional functionality to the users.
@MainActor
private final class ImageViewController {
    private weak var imageView: ImageDisplayingView?
    private var task: ImageTask?
    private var options: ImageLoadingOptions

    #if os(iOS) || os(tvOS)
    // Image view used for cross-fade transition between images with different
    // content modes.
    private lazy var transitionImageView = UIImageView()
    #endif

    // Automatically cancel the request when the view is deallocated.
    deinit {
        task?.cancel()
    }

    init(view: /* weak */ ImageDisplayingView) {
        self.imageView = view
        self.options = .shared
    }

    // MARK: - Associating Controller

    static var controllerAK = "ImageViewController.AssociatedKey"

    // Lazily create a controller for a given view and associate it with a view.
    static func controller(for view: ImageDisplayingView) -> ImageViewController {
        if let controller = objc_getAssociatedObject(view, &ImageViewController.controllerAK) as? ImageViewController {
            return controller
        }
        let controller = ImageViewController(view: view)
        objc_setAssociatedObject(view, &ImageViewController.controllerAK, controller, .OBJC_ASSOCIATION_RETAIN)
        return controller
    }

    // MARK: - Loading Images

    func loadImage(
        with request: ImageRequest?,
        options: ImageLoadingOptions,
        progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil,
        completion: ((_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil
    ) -> ImageTask? {
        cancelOutstandingTask()

        guard let imageView = imageView else {
            return nil
        }

        self.options = options

        if options.isPrepareForReuseEnabled { // enabled by default
            #if os(iOS) || os(tvOS)
            imageView.layer.removeAllAnimations()
            #elseif os(macOS)
            let layer = (imageView as? NSView)?.layer ?? imageView.layer
            layer?.removeAllAnimations()
            #endif
        }

        // Handle a scenario where request is `nil` (in the same way as a failure)
        guard var request = request else {
            if options.isPrepareForReuseEnabled {
                imageView.nuke_display(image: nil, data: nil)
            }
            let result: Result<ImageResponse, ImagePipeline.Error> = .failure(.imageRequestMissing)
            handle(result: result, isFromMemory: true)
            completion?(result)
            return nil
        }

        let pipeline = options.pipeline ?? ImagePipeline.shared
        if !options.processors.isEmpty && request.processors.isEmpty {
            request.processors = options.processors
        }

        // Quick synchronous memory cache lookup.
        if let image = pipeline.cache[request] {
            display(image, true, .success)
            if !image.isPreview { // Final image was downloaded
                completion?(.success(ImageResponse(container: image, request: request, cacheType: .memory)))
                return nil // No task to perform
            }
        }

        // Display a placeholder.
        if let placeholder = options.placeholder {
            display(ImageContainer(image: placeholder), true, .placeholder)
        } else if options.isPrepareForReuseEnabled {
            imageView.nuke_display(image: nil, data: nil) // Remove previously displayed images (if any)
        }

        task = pipeline.loadImage(with: request, queue: .main, progress: { [weak self] response, completedCount, totalCount in
            if let response = response, options.isProgressiveRenderingEnabled {
                self?.handle(partialImage: response)
            }
            progress?(response, completedCount, totalCount)
        }, completion: { [weak self] result in
            self?.handle(result: result, isFromMemory: false)
            completion?(result)
        })
        return task
    }

    func cancelOutstandingTask() {
        task?.cancel() // The pipeline guarantees no callbacks to be deliver after cancellation
        task = nil
    }

    // MARK: - Handling Responses

    private func handle(result: Result<ImageResponse, ImagePipeline.Error>, isFromMemory: Bool) {
        switch result {
        case let .success(response):
            display(response.container, isFromMemory, .success)
        case .failure:
            if let failureImage = options.failureImage {
                display(ImageContainer(image: failureImage), isFromMemory, .failure)
            }
        }
        self.task = nil
    }

    private func handle(partialImage response: ImageResponse) {
        display(response.container, false, .success)
    }

    #if os(iOS) || os(tvOS) || os(macOS)

    private func display(_ image: ImageContainer, _ isFromMemory: Bool, _ response: ImageLoadingOptions.ResponseType) {
        guard let imageView = imageView else {
            return
        }

        var image = image

        #if os(iOS) || os(tvOS)
        if let tintColor = options.tintColor(for: response) {
            image.image = image.image.withRenderingMode(.alwaysTemplate)
            imageView.tintColor = tintColor
        }
        #endif

        if !isFromMemory || options.alwaysTransition, let transition = options.transition(for: response) {
            switch transition.style {
            case let .fadeIn(params):
                runFadeInTransition(image: image, params: params, response: response)
            case let .custom(closure):
                // The user is responsible for both displaying an image and performing
                // animations.
                closure(imageView, image.image)
            }
        } else {
            imageView.display(image)
        }

        #if os(iOS) || os(tvOS)
        if let contentMode = options.contentMode(for: response) {
            imageView.contentMode = contentMode
        }
        #endif
    }

    #elseif os(watchOS)

    private func display(_ image: ImageContainer, _ isFromMemory: Bool, _ response: ImageLoadingOptions.ResponseType) {
        imageView?.display(image)
    }

    #endif
}

// MARK: - ImageViewController (Transitions)

extension ImageViewController {
    #if os(iOS) || os(tvOS)

    private func runFadeInTransition(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters, response: ImageLoadingOptions.ResponseType) {
        guard let imageView = imageView else {
            return
        }

        // Special case where it animates between content modes, only works
        // on imageView subclasses.
        if let contentMode = options.contentMode(for: response), imageView.contentMode != contentMode, let imageView = imageView as? UIImageView, imageView.image != nil {
            runCrossDissolveWithContentMode(imageView: imageView, image: image, params: params)
        } else {
            runSimpleFadeIn(image: image, params: params)
        }
    }

    private func runSimpleFadeIn(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters) {
        guard let imageView = imageView else {
            return
        }

        UIView.transition(
            with: imageView,
            duration: params.duration,
            options: params.options.union(.transitionCrossDissolve),
            animations: {
                imageView.nuke_display(image: image.image, data: image.data)
            },
            completion: nil
        )
    }

    /// Performs cross-dissolve animation alonside transition to a new content
    /// mode. This isn't natively supported feature and it requires a second
    /// image view. There might be better ways to implement it.
    private func runCrossDissolveWithContentMode(imageView: UIImageView, image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters) {
        // Lazily create a transition view.
        let transitionView = self.transitionImageView

        // Create a transition view which mimics current view's contents.
        transitionView.image = imageView.image
        transitionView.contentMode = imageView.contentMode
        imageView.addSubview(transitionView)
        transitionView.frame = imageView.bounds

        // "Manual" cross-fade.
        transitionView.alpha = 1
        imageView.alpha = 0
        imageView.display(image) // Display new image in current view

        UIView.animate(
            withDuration: params.duration,
            delay: 0,
            options: params.options,
            animations: {
                transitionView.alpha = 0
                imageView.alpha = 1
            },
            completion: { isCompleted in
                if isCompleted {
                    transitionView.removeFromSuperview()
                }
            }
        )
    }

    #elseif os(macOS)

    private func runFadeInTransition(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters, response: ImageLoadingOptions.ResponseType) {
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.duration = params.duration
        animation.fromValue = 0
        animation.toValue = 1
        imageView?.layer?.add(animation, forKey: "imageTransition")

        imageView?.display(image)
    }

    #endif
}

#endif

<MSG> Move Nuke tests

<DFF> @@ -3,6 +3,7 @@
 // Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).
 
 import Foundation
+import Nuke
 
 #if !os(macOS)
 import UIKit.UIImage

{'commit': 'diff --git a/Sources/NukeExtensions/ImageLoadingOptions.swift b/Sources/NukeExtensions/ImageLoadingOptions.swift\nindex 0b931684..adfc5bfe 100644\n--- a/Sources/NukeExtensions/ImageLoadingOptions.swift\n+++ b/Sources/NukeExtensions/ImageLoadingOptions.swift\n@@ -3,6 +3,7 @@\n // Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).\n \n import Foundation\n+import Nuke\n \n #if !os(macOS)\n import UIKit.UIImage\ndiff --git a/Sources/NukeExtensions/ImageViewExtensions.swift b/Sources/NukeExtensions/ImageViewExtensions.swift\nindex 54285193..57837819 100644\n--- a/Sources/NukeExtensions/ImageViewExtensions.swift\n+++ b/Sources/NukeExtensions/ImageViewExtensions.swift\n@@ -3,6 +3,7 @@\n // Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).\n \n import Foundation\n+import Nuke\n \n #if !os(macOS)\n import UIKit.UIImage\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineCoalescingTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineCoalescingTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineConfigurationTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineConfigurationTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineConfigurationTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineConfigurationTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineDataCacheTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineDataCacheTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineDecodingTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDecodingTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineDecodingTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineDecodingTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineDelegateTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDelegateTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineDelegateTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineDelegateTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineFormatsTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineFormatsTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineFormatsTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineFormatsTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineImageCacheTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineImageCacheTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineImageCacheTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineImageCacheTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineLoadDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineLoadDataTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineProcessorTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineProcessorTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineProcessorTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineProcessorTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineProgressiveDecodingTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineProgressiveDecodingTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineProgressiveDecodingTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineProgressiveDecodingTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelinePublisherTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelinePublisherTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelinePublisherTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelinePublisherTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineResumableDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineResumableDataTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift\ndiff --git a/Tests/ImagePipelineTests/ImagePipelineTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift\nsimilarity index 100%\nrename from Tests/ImagePipelineTests/ImagePipelineTests.swift\nrename to Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift\n', 'commit_subject': 'Move Nuke tests', 'repo_name': 'kean/Nuke', 'committer': {'name': 'kean', 'email': '7d79009c44fb8cc76c73cdb194cbb1e3b38c7a62@gmail.com', 'time_sec': '1655063558', 'tz_offset': '-240', 'date': {'seconds': '1655063558'}}, 'commit_tree': 'f5e01f97feb84f78b2e20e221453f530d0884ada', 'commit_message': 'Move Nuke tests\n', 'language_name': 'Swift', 'license': 'mit', 'hunks': '@@ -3,6 +3,7 @@\n // Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).\n \n import Foundation\n+import Nuke\n \n #if !os(macOS)\n import UIKit.UIImage\n', 'addition_count': 1, 'deletion_count': 0, 'src_file': 'a/Sources/NukeExtensions/ImageViewExtensions.swift', 'tgt_file': 'b/Sources/NukeExtensions/ImageViewExtensions.swift', 'file_extension': '.swift', 'before_file': '// The MIT License (MIT)\n//\n// Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n\n#if !os(macOS)\nimport UIKit.UIImage\nimport UIKit.UIColor\n#else\nimport AppKit.NSImage\n#endif\n\n#if os(iOS) || os(tvOS) || os(macOS)\n\n/// Displays images. Add the conformance to this protocol to your views to make\n/// them compatible with Nuke image loading extensions.\n///\n/// The protocol is defined as `@objc` to make it possible to override its\n/// methods in extensions (e.g. you can override `nuke_display(image:data:)` in\n/// `UIImageView` subclass like `Gifu.ImageView).\n///\n/// The protocol and its methods have prefixes to make sure they don\'t clash\n/// with other similar methods and protocol in Objective-C runtime.\n@MainActor\n@objc public protocol Nuke_ImageDisplaying {\n    /// Display a given image.\n    @objc func nuke_display(image: PlatformImage?, data: Data?)\n\n    #if os(macOS)\n    @objc var layer: CALayer? { get }\n    #endif\n}\n\nextension Nuke_ImageDisplaying {\n    func display(_ container: ImageContainer) {\n        nuke_display(image: container.image, data: container.data)\n    }\n}\n\n#if os(macOS)\nextension Nuke_ImageDisplaying {\n    public var layer: CALayer? { nil }\n}\n#endif\n\n#if os(iOS) || os(tvOS)\nimport UIKit\n/// A `UIView` that implements `ImageDisplaying` protocol.\npublic typealias ImageDisplayingView = UIView & Nuke_ImageDisplaying\n\nextension UIImageView: Nuke_ImageDisplaying {\n    /// Displays an image.\n    open func nuke_display(image: UIImage?, data: Data? = nil) {\n        self.image = image\n    }\n}\n#elseif os(macOS)\nimport Cocoa\n/// An `NSObject` that implements `ImageDisplaying`  and `Animating` protocols.\n/// Can support `NSView` and `NSCell`. The latter can return nil for layer.\npublic typealias ImageDisplayingView = NSObject & Nuke_ImageDisplaying\n\nextension NSImageView: Nuke_ImageDisplaying {\n    /// Displays an image.\n    open func nuke_display(image: NSImage?, data: Data? = nil) {\n        self.image = image\n    }\n}\n#endif\n\n#if os(tvOS)\nimport TVUIKit\n\nextension TVPosterView: Nuke_ImageDisplaying {\n    /// Displays an image.\n    open func nuke_display(image: UIImage?, data: Data? = nil) {\n        self.image = image\n    }\n}\n#endif\n\n// MARK: - ImageView Extensions\n\n/// Loads an image with the given request and displays it in the view.\n///\n/// See the complete method signature for more information.\n@MainActor\n@discardableResult public func loadImage(\n    with request: (any ImageRequestConvertible)?,\n    options: ImageLoadingOptions = ImageLoadingOptions.shared,\n    into view: ImageDisplayingView,\n    completion: @escaping (_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void\n) -> ImageTask? {\n    loadImage(with: request, options: options, into: view, progress: nil, completion: completion)\n}\n\n/// Loads an image with the given request and displays it in the view.\n///\n/// Before loading a new image, the view is prepared for reuse by canceling any\n/// outstanding requests and removing a previously displayed image.\n///\n/// If the image is stored in the memory cache, it is displayed immediately with\n/// no animations. If not, the image is loaded using an image pipeline. When the\n/// image is loading, the `placeholder` is displayed. When the request\n/// completes the loaded image is displayed (or `failureImage` in case of an error)\n/// with the selected animation.\n///\n/// - parameters:\n///   - request: The image request. If `nil`, it\'s handled as a failure scenario.\n///   - options: `ImageLoadingOptions.shared` by default.\n///   - view: Nuke keeps a weak reference to the view. If the view is deallocated\n///   the associated request automatically gets canceled.\n///   - progress: A closure to be called periodically on the main thread\n///   when the progress is updated.\n///   - completion: A closure to be called on the main thread when the\n///   request is finished. Gets called synchronously if the response was found in\n///   the memory cache.\n///\n/// - returns: An image task or `nil` if the image was found in the memory cache.\n@MainActor\n@discardableResult public func loadImage(\n    with request: (any ImageRequestConvertible)?,\n    options: ImageLoadingOptions = ImageLoadingOptions.shared,\n    into view: ImageDisplayingView,\n    progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil,\n    completion: ((_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil\n) -> ImageTask? {\n    let controller = ImageViewController.controller(for: view)\n    return controller.loadImage(with: request?.asImageRequest(), options: options, progress: progress, completion: completion)\n}\n\n/// Cancels an outstanding request associated with the view.\n@MainActor\npublic func cancelRequest(for view: ImageDisplayingView) {\n    ImageViewController.controller(for: view).cancelOutstandingTask()\n}\n\n// MARK: - ImageViewController\n\n/// Manages image requests on behalf of an image view.\n///\n/// - note: With a few modifications this might become public at some point,\n/// however as it stands today `ImageViewController` is just a helper class,\n/// making it public wouldn\'t expose any additional functionality to the users.\n@MainActor\nprivate final class ImageViewController {\n    private weak var imageView: ImageDisplayingView?\n    private var task: ImageTask?\n    private var options: ImageLoadingOptions\n\n    #if os(iOS) || os(tvOS)\n    // Image view used for cross-fade transition between images with different\n    // content modes.\n    private lazy var transitionImageView = UIImageView()\n    #endif\n\n    // Automatically cancel the request when the view is deallocated.\n    deinit {\n        task?.cancel()\n    }\n\n    init(view: /* weak */ ImageDisplayingView) {\n        self.imageView = view\n        self.options = .shared\n    }\n\n    // MARK: - Associating Controller\n\n    static var controllerAK = "ImageViewController.AssociatedKey"\n\n    // Lazily create a controller for a given view and associate it with a view.\n    static func controller(for view: ImageDisplayingView) -> ImageViewController {\n        if let controller = objc_getAssociatedObject(view, &ImageViewController.controllerAK) as? ImageViewController {\n            return controller\n        }\n        let controller = ImageViewController(view: view)\n        objc_setAssociatedObject(view, &ImageViewController.controllerAK, controller, .OBJC_ASSOCIATION_RETAIN)\n        return controller\n    }\n\n    // MARK: - Loading Images\n\n    func loadImage(\n        with request: ImageRequest?,\n        options: ImageLoadingOptions,\n        progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil,\n        completion: ((_ result: Result<ImageResponse, ImagePipeline.Error>) -> Void)? = nil\n    ) -> ImageTask? {\n        cancelOutstandingTask()\n\n        guard let imageView = imageView else {\n            return nil\n        }\n\n        self.options = options\n\n        if options.isPrepareForReuseEnabled { // enabled by default\n            #if os(iOS) || os(tvOS)\n            imageView.layer.removeAllAnimations()\n            #elseif os(macOS)\n            let layer = (imageView as? NSView)?.layer ?? imageView.layer\n            layer?.removeAllAnimations()\n            #endif\n        }\n\n        // Handle a scenario where request is `nil` (in the same way as a failure)\n        guard var request = request else {\n            if options.isPrepareForReuseEnabled {\n                imageView.nuke_display(image: nil, data: nil)\n            }\n            let result: Result<ImageResponse, ImagePipeline.Error> = .failure(.imageRequestMissing)\n            handle(result: result, isFromMemory: true)\n            completion?(result)\n            return nil\n        }\n\n        let pipeline = options.pipeline ?? ImagePipeline.shared\n        if !options.processors.isEmpty && request.processors.isEmpty {\n            request.processors = options.processors\n        }\n\n        // Quick synchronous memory cache lookup.\n        if let image = pipeline.cache[request] {\n            display(image, true, .success)\n            if !image.isPreview { // Final image was downloaded\n                completion?(.success(ImageResponse(container: image, request: request, cacheType: .memory)))\n                return nil // No task to perform\n            }\n        }\n\n        // Display a placeholder.\n        if let placeholder = options.placeholder {\n            display(ImageContainer(image: placeholder), true, .placeholder)\n        } else if options.isPrepareForReuseEnabled {\n            imageView.nuke_display(image: nil, data: nil) // Remove previously displayed images (if any)\n        }\n\n        task = pipeline.loadImage(with: request, queue: .main, progress: { [weak self] response, completedCount, totalCount in\n            if let response = response, options.isProgressiveRenderingEnabled {\n                self?.handle(partialImage: response)\n            }\n            progress?(response, completedCount, totalCount)\n        }, completion: { [weak self] result in\n            self?.handle(result: result, isFromMemory: false)\n            completion?(result)\n        })\n        return task\n    }\n\n    func cancelOutstandingTask() {\n        task?.cancel() // The pipeline guarantees no callbacks to be deliver after cancellation\n        task = nil\n    }\n\n    // MARK: - Handling Responses\n\n    private func handle(result: Result<ImageResponse, ImagePipeline.Error>, isFromMemory: Bool) {\n        switch result {\n        case let .success(response):\n            display(response.container, isFromMemory, .success)\n        case .failure:\n            if let failureImage = options.failureImage {\n                display(ImageContainer(image: failureImage), isFromMemory, .failure)\n            }\n        }\n        self.task = nil\n    }\n\n    private func handle(partialImage response: ImageResponse) {\n        display(response.container, false, .success)\n    }\n\n    #if os(iOS) || os(tvOS) || os(macOS)\n\n    private func display(_ image: ImageContainer, _ isFromMemory: Bool, _ response: ImageLoadingOptions.ResponseType) {\n        guard let imageView = imageView else {\n            return\n        }\n\n        var image = image\n\n        #if os(iOS) || os(tvOS)\n        if let tintColor = options.tintColor(for: response) {\n            image.image = image.image.withRenderingMode(.alwaysTemplate)\n            imageView.tintColor = tintColor\n        }\n        #endif\n\n        if !isFromMemory || options.alwaysTransition, let transition = options.transition(for: response) {\n            switch transition.style {\n            case let .fadeIn(params):\n                runFadeInTransition(image: image, params: params, response: response)\n            case let .custom(closure):\n                // The user is responsible for both displaying an image and performing\n                // animations.\n                closure(imageView, image.image)\n            }\n        } else {\n            imageView.display(image)\n        }\n\n        #if os(iOS) || os(tvOS)\n        if let contentMode = options.contentMode(for: response) {\n            imageView.contentMode = contentMode\n        }\n        #endif\n    }\n\n    #elseif os(watchOS)\n\n    private func display(_ image: ImageContainer, _ isFromMemory: Bool, _ response: ImageLoadingOptions.ResponseType) {\n        imageView?.display(image)\n    }\n\n    #endif\n}\n\n// MARK: - ImageViewController (Transitions)\n\nextension ImageViewController {\n    #if os(iOS) || os(tvOS)\n\n    private func runFadeInTransition(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters, response: ImageLoadingOptions.ResponseType) {\n        guard let imageView = imageView else {\n            return\n        }\n\n        // Special case where it animates between content modes, only works\n        // on imageView subclasses.\n        if let contentMode = options.contentMode(for: response), imageView.contentMode != contentMode, let imageView = imageView as? UIImageView, imageView.image != nil {\n            runCrossDissolveWithContentMode(imageView: imageView, image: image, params: params)\n        } else {\n            runSimpleFadeIn(image: image, params: params)\n        }\n    }\n\n    private func runSimpleFadeIn(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters) {\n        guard let imageView = imageView else {\n            return\n        }\n\n        UIView.transition(\n            with: imageView,\n            duration: params.duration,\n            options: params.options.union(.transitionCrossDissolve),\n            animations: {\n                imageView.nuke_display(image: image.image, data: image.data)\n            },\n            completion: nil\n        )\n    }\n\n    /// Performs cross-dissolve animation alonside transition to a new content\n    /// mode. This isn\'t natively supported feature and it requires a second\n    /// image view. There might be better ways to implement it.\n    private func runCrossDissolveWithContentMode(imageView: UIImageView, image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters) {\n        // Lazily create a transition view.\n        let transitionView = self.transitionImageView\n\n        // Create a transition view which mimics current view\'s contents.\n        transitionView.image = imageView.image\n        transitionView.contentMode = imageView.contentMode\n        imageView.addSubview(transitionView)\n        transitionView.frame = imageView.bounds\n\n        // "Manual" cross-fade.\n        transitionView.alpha = 1\n        imageView.alpha = 0\n        imageView.display(image) // Display new image in current view\n\n        UIView.animate(\n            withDuration: params.duration,\n            delay: 0,\n            options: params.options,\n            animations: {\n                transitionView.alpha = 0\n                imageView.alpha = 1\n            },\n            completion: { isCompleted in\n                if isCompleted {\n                    transitionView.removeFromSuperview()\n                }\n            }\n        )\n    }\n\n    #elseif os(macOS)\n\n    private func runFadeInTransition(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters, response: ImageLoadingOptions.ResponseType) {\n        let animation = CABasicAnimation(keyPath: "opacity")\n        animation.duration = params.duration\n        animation.fromValue = 0\n        animation.toValue = 1\n        imageView?.layer?.add(animation, forKey: "imageTransition")\n\n        imageView?.display(image)\n    }\n\n    #endif\n}\n\n#endif\n', 'message': 'Move Nuke tests\n', 'author_name': 'kean', 'lang': 'swift'}