// // KF.swift // Kingfisher // // Created by onevcat on 2020/09/21. // // Copyright (c) 2020 Wei Wang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if canImport(UIKit) import UIKit #endif #if canImport(CarPlay) && !targetEnvironment(macCatalyst) import CarPlay #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit #endif #if canImport(WatchKit) import WatchKit #endif #if canImport(TVUIKit) import TVUIKit #endif /// A helper type to create image setting tasks in a builder pattern. /// Use methods in this type to create a `KF.Builder` instance and configure image tasks there. public enum KF { /// Creates a builder for a given `Source`. /// - Parameter source: The `Source` object defines data information from network or a data provider. /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` /// to start the image loading. public static func source(_ source: Source?) -> KF.Builder { Builder(source: source) } /// Creates a builder for a given `Resource`. /// - Parameter resource: The `Resource` object defines data information like key or URL. /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` /// to start the image loading. public static func resource(_ resource: Resource?) -> KF.Builder { source(resource?.convertToSource()) } /// Creates a builder for a given `URL` and an optional cache key. /// - Parameters: /// - url: The URL where the image should be downloaded. /// - cacheKey: The key used to store the downloaded image in cache. /// If `nil`, the `absoluteString` of `url` is used as the cache key. /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` /// to start the image loading. public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder { source(url?.convertToSource(overrideCacheKey: cacheKey)) } /// Creates a builder for a given `ImageDataProvider`. /// - Parameter provider: The `ImageDataProvider` object contains information about the data. /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` /// to start the image loading. public static func dataProvider(_ provider: ImageDataProvider?) -> KF.Builder { source(provider?.convertToSource()) } /// Creates a builder for some given raw data and a cache key. /// - Parameters: /// - data: The data object from which the image should be created. /// - cacheKey: The key used to store the downloaded image in cache. /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` /// to start the image loading. public static func data(_ data: Data?, cacheKey: String) -> KF.Builder { if let data = data { return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) } else { return dataProvider(nil) } } } extension KF { /// A builder class to configure an image retrieving task and set it to a holder view or component. public class Builder { private let source: Source? #if os(watchOS) private var placeholder: KFCrossPlatformImage? #else private var placeholder: Placeholder? #endif public var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions) public let onFailureDelegate = Delegate() public let onSuccessDelegate = Delegate() public let onProgressDelegate = Delegate<(Int64, Int64), Void>() init(source: Source?) { self.source = source } private var resultHandler: ((Result) -> Void)? { { switch $0 { case .success(let result): self.onSuccessDelegate(result) case .failure(let error): self.onFailureDelegate(error) } } } private var progressBlock: DownloadProgressBlock { { self.onProgressDelegate(($0, $1)) } } } } extension KF.Builder { #if !os(watchOS) /// Builds the image task request and sets it to an image view. /// - Parameter imageView: The image view which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? { imageView.kf.setImage( with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } /// Builds the image task request and sets it to an `NSTextAttachment` object. /// - Parameters: /// - attachment: The text attachment object which loads the task and should be set with the image. /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to attachment: NSTextAttachment, attributedView: @autoclosure @escaping () -> KFCrossPlatformView) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return attachment.kf.setImage( with: source, attributedView: attributedView, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #if canImport(UIKit) /// Builds the image task request and sets it to a button. /// - Parameters: /// - button: The button which loads the task and should be set with the image. /// - state: The button state to which the image should be set. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return button.kf.setImage( with: source, for: state, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } /// Builds the image task request and sets it to the background image for a button. /// - Parameters: /// - button: The button which loads the task and should be set with the image. /// - state: The button state to which the image should be set. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return button.kf.setBackgroundImage( with: source, for: state, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif // end of canImport(UIKit) #if canImport(CarPlay) && !targetEnvironment(macCatalyst) /// Builds the image task request and sets it to the image for a list item. /// - Parameters: /// - listItem: The list item which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @available(iOS 14.0, *) @discardableResult public func set(to listItem: CPListItem) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return listItem.kf.setImage( with: source, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) /// Builds the image task request and sets it to a button. /// - Parameter button: The button which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to button: NSButton) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return button.kf.setImage( with: source, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } /// Builds the image task request and sets it to the alternative image for a button. /// - Parameter button: The button which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func setAlternative(to button: NSButton) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return button.kf.setAlternateImage( with: source, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif // end of canImport(AppKit) #endif // end of !os(watchOS) #if canImport(WatchKit) /// Builds the image task request and sets it to a `WKInterfaceImage` object. /// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @discardableResult public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? { return interfaceImage.kf.setImage( with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif // end of canImport(WatchKit) #if canImport(TVUIKit) /// Builds the image task request and sets it to a TV monogram view. /// - Parameter monogramView: The monogram view which loads the task and should be set with the image. /// - Returns: A task represents the image downloading, if initialized. /// This value is `nil` if the image is being loaded from cache. @available(tvOS 12.0, *) @discardableResult public func set(to monogramView: TVMonogramView) -> DownloadTask? { let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil return monogramView.kf.setImage( with: source, placeholder: placeholderImage, parsedOptions: options, progressBlock: progressBlock, completionHandler: resultHandler ) } #endif // end of canImport(TVUIKit) } #if !os(watchOS) extension KF.Builder { #if os(iOS) || os(tvOS) || os(visionOS) /// Sets a placeholder which is used while retrieving the image. /// - Parameter placeholder: A placeholder to show while retrieving the image from its source. /// - Returns: A `KF.Builder` with changes applied. public func placeholder(_ placeholder: Placeholder?) -> Self { self.placeholder = placeholder return self } #endif /// Sets a placeholder image which is used while retrieving the image. /// - Parameter placeholder: An image to show while retrieving the image from its source. /// - Returns: A `KF.Builder` with changes applied. public func placeholder(_ image: KFCrossPlatformImage?) -> Self { self.placeholder = image return self } } #endif extension KF.Builder { #if os(iOS) || os(tvOS) || os(visionOS) /// Sets the transition for the image task. /// - Parameter transition: The desired transition effect when setting the image to image view. /// - Returns: A `KF.Builder` with changes applied. /// /// Kingfisher will use the `transition` to animate the image in if it is downloaded from web. /// The transition will not happen when the /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KF.Builder`. public func transition(_ transition: ImageTransition) -> Self { options.transition = transition return self } /// Sets a fade transition for the image task. /// - Parameter duration: The duration of the fade transition. /// - Returns: A `KF.Builder` with changes applied. /// /// Kingfisher will use the fade transition to animate the image in if it is downloaded from web. /// The transition will not happen when the /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KF.Builder`. public func fade(duration: TimeInterval) -> Self { options.transition = .fade(duration) return self } #endif /// Sets whether keeping the existing image of image view while setting another image to it. /// - Parameter enabled: Whether the existing image should be kept. /// - Returns: A `KF.Builder` with changes applied. /// /// By setting this option, the placeholder image parameter of image view extension method /// will be ignored and the current image will be kept while loading or downloading the new image. /// public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self { options.keepCurrentImageWhileLoading = enabled return self } /// Sets whether only the first frame from an animated image file should be loaded as a single image. /// - Parameter enabled: Whether the only the first frame should be loaded. /// - Returns: A `KF.Builder` with changes applied. /// /// Loading an animated images may take too much memory. It will be useful when you want to display a /// static preview of the first frame from an animated image. /// /// This option will be ignored if the target image is not animated image data. /// public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self { options.onlyLoadFirstFrame = enabled return self } /// Enables progressive image loading with a specified `ImageProgressive` setting to process the /// progressive JPEG data and display it in a progressive way. /// - Parameter progressive: The progressive settings which is used while loading. /// - Returns: A `KF.Builder` with changes applied. public func progressiveJPEG(_ progressive: ImageProgressive? = .init()) -> Self { options.progressiveJPEG = progressive return self } } // MARK: - Deprecated extension KF.Builder { /// Starts the loading process of `self` immediately. /// /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once /// could help avoiding the flickering, with some performance trade-off. /// /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. /// It does nothing now and please just remove it. /// /// - Returns: The `Self` value with changes applied. @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") public func loadImmediately(_ start: Bool = true) -> Self { return self } } // MARK: - Redirect Handler extension KF { /// Represents the detail information when a task redirect happens. It is wrapping necessary information for a /// `ImageDownloadRedirectHandler`. See that protocol for more information. public struct RedirectPayload { /// The related session data task when the redirect happens. It is /// the current `SessionDataTask` which triggers this redirect. public let task: SessionDataTask /// The response received during redirection. public let response: HTTPURLResponse /// The request for redirection which can be modified. public let newRequest: URLRequest /// A closure for being called with modified request. public let completionHandler: (URLRequest?) -> Void } }