Sfoglia il codice sorgente

Support animated images in SwiftUI

Binlogo 4 anni fa
parent
commit
c265015941

+ 83 - 0
Demo/Demo/Kingfisher-Demo/SwiftUIViews/AnimatedImageDemo.swift

@@ -0,0 +1,83 @@
+//
+//  AnimatedImageDemo.swift
+//  Kingfisher
+//
+//  Created by wangxingbin on 2021/4/27.
+//
+//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>
+//
+//  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.
+
+import SwiftUI
+import Kingfisher
+
+@available(iOS 13.0, *)
+struct AnimatedImageDemo: View {
+    
+    @State private var index = 1
+    
+    @State private var blackWhite = false
+    
+    var url: URL {
+        ImageLoader.gifImageURLs[index - 1]
+    }
+    
+    var body: some View {
+        VStack {
+            KFAnimatedImage(url)
+                .cacheOriginalImage()
+                .setProcessor(blackWhite ? BlackWhiteProcessor() : DefaultImageProcessor())
+                .onSuccess { r in
+                    print("suc: \(r)")
+                }
+                .onFailure { e in
+                    print("err: \(e)")
+                }
+                .placeholder {
+                    Image(systemName: "arrow.2.circlepath.circle")
+                        .font(.largeTitle)
+                }
+                .fade(duration: 1)
+                .forceTransition()
+                .resizable()
+                .frame(width: 300, height: 300)
+                .cornerRadius(20)
+                .shadow(radius: 5)
+                .frame(width: 320, height: 320)
+
+            Button(action: {
+                self.index = (self.index % 3) + 1
+            }) { Text("Next Image") }
+            Button(action: {
+                self.blackWhite.toggle()
+            }) { Text("Black & White") }
+        }.navigationBarTitle(Text("Basic Image"), displayMode: .inline)
+    }
+    
+}
+
+@available(iOS 13.0, *)
+struct AnimatedImageDemo_Previews: PreviewProvider {
+    
+    static var previews: some View {
+        AnimatedImageDemo()
+    }
+    
+}
+

+ 4 - 0
Demo/Kingfisher-Demo.xcodeproj/project.pbxproj

@@ -7,6 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		072922432638639D0089E810 /* AnimatedImageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072922422638639D0089E810 /* AnimatedImageDemo.swift */; };
 		277EAE8D2045B39C00547CD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE892045B39C00547CD3 /* Assets.xcassets */; };
 		277EAE8E2045B39C00547CD3 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE8A2045B39C00547CD3 /* Interface.storyboard */; };
 		277EAE9D2045B4D500547CD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE962045B4D500547CD3 /* Assets.xcassets */; };
@@ -146,6 +147,7 @@
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		072922422638639D0089E810 /* AnimatedImageDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageDemo.swift; sourceTree = "<group>"; };
 		277EAE892045B39C00547CD3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		277EAE8B2045B39C00547CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
 		277EAE8C2045B39C00547CD3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -414,6 +416,7 @@
 				D1F78A632589F17200930759 /* SingleViewDemo.swift */,
 				D198F41D25EDC11500C53E0D /* LazyVStackDemo.swift */,
 				D198F41F25EDC34000C53E0D /* SizingAnimationDemo.swift */,
+				072922422638639D0089E810 /* AnimatedImageDemo.swift */,
 			);
 			path = SwiftUIViews;
 			sourceTree = "<group>";
@@ -669,6 +672,7 @@
 				D1F78A662589F17200930759 /* SingleViewDemo.swift in Sources */,
 				D12E0C981C47F91800AC98AD /* ImageCollectionViewCell.swift in Sources */,
 				D198F42025EDC34000C53E0D /* SizingAnimationDemo.swift in Sources */,
+				072922432638639D0089E810 /* AnimatedImageDemo.swift in Sources */,
 				D1A1CCA721A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */,
 				4B92FE5625FF906B00473088 /* AutoSizingTableViewController.swift in Sources */,
 				D1F78A642589F17200930759 /* ListDemo.swift in Sources */,

+ 8 - 0
Kingfisher.xcodeproj/project.pbxproj

@@ -7,6 +7,8 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		07292245263B02F00089E810 /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07292244263B02F00089E810 /* KFAnimatedImage.swift */; };
+		07292249263B125F0089E810 /* KFAnimatedImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07292248263B125E0089E810 /* KFAnimatedImageOptions.swift */; };
 		4B10480D216F157000300C61 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B10480C216F157000300C61 /* ImageDataProcessor.swift */; };
 		4B46CC5F217449C600D90C4A /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC5E217449C600D90C4A /* MemoryStorage.swift */; };
 		4B46CC64217449E000D90C4A /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC63217449E000D90C4A /* Storage.swift */; };
@@ -127,6 +129,8 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
+		07292244263B02F00089E810 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFAnimatedImage.swift; sourceTree = "<group>"; };
+		07292248263B125E0089E810 /* KFAnimatedImageOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFAnimatedImageOptions.swift; sourceTree = "<group>"; };
 		185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSButtonExtensionTests.swift; sourceTree = "<group>"; };
 		4B10480C216F157000300C61 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataProcessor.swift; sourceTree = "<group>"; };
 		4B164ACE1B8D554200768EC6 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; };
@@ -654,6 +658,8 @@
 				D1F7607523097532000C5269 /* ImageBinder.swift */,
 				D1F7607623097532000C5269 /* KFImage.swift */,
 				D1889533258F7648003B73BE /* KFImageOptions.swift */,
+				07292244263B02F00089E810 /* KFAnimatedImage.swift */,
+				07292248263B125E0089E810 /* KFAnimatedImageOptions.swift */,
 			);
 			path = SwiftUI;
 			sourceTree = "<group>";
@@ -803,6 +809,7 @@
 				D12AB70C215D2BB50013BA68 /* KingfisherManager.swift in Sources */,
 				4B8351CC217084660081EED8 /* Runtime.swift in Sources */,
 				D12AB6C0215D2BB50013BA68 /* RequestModifier.swift in Sources */,
+				07292249263B125F0089E810 /* KFAnimatedImageOptions.swift in Sources */,
 				4B10480D216F157000300C61 /* ImageDataProcessor.swift in Sources */,
 				D12AB72C215D2BB50013BA68 /* Indicator.swift in Sources */,
 				D12AB6C8215D2BB50013BA68 /* ImageDownloader.swift in Sources */,
@@ -823,6 +830,7 @@
 				D12AB6C4215D2BB50013BA68 /* Resource.swift in Sources */,
 				76FB4FD2262D773E006D15F8 /* GraphicsContext.swift in Sources */,
 				D8FCF6A821C5A0E500F9ABC0 /* RedirectHandler.swift in Sources */,
+				07292245263B02F00089E810 /* KFAnimatedImage.swift in Sources */,
 				D1A37BDE215D34E8009B39B7 /* ImageDrawing.swift in Sources */,
 				4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */,
 				D12AB708215D2BB50013BA68 /* KingfisherError.swift in Sources */,

+ 14 - 0
Sources/General/KFOptionsSetter.swift

@@ -55,6 +55,20 @@ extension KFImage: KFOptionSetter {
 
     public var delegateObserver: AnyObject { context.binder }
 }
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension KFAnimatedImage: KFOptionSetter {
+    public var options: KingfisherParsedOptionsInfo {
+        get { context.binder.options }
+        nonmutating set { context.binder.options = newValue }
+    }
+
+    public var onFailureDelegate: Delegate<KingfisherError, Void> { context.binder.onFailureDelegate }
+    public var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { context.binder.onSuccessDelegate }
+    public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.binder.onProgressDelegate }
+
+    public var delegateObserver: AnyObject { context.binder }
+}
 #endif
 
 // MARK: - Life cycles

+ 209 - 0
Sources/SwiftUI/KFAnimatedImage.swift

@@ -0,0 +1,209 @@
+//
+//  KFAnimatedImage.swift
+//  Kingfisher
+//
+//  Created by wangxingbin on 2021/4/29.
+//
+//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>
+//
+//  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(SwiftUI) && canImport(Combine)
+import SwiftUI
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+internal extension KFAnimatedImage {
+    typealias ImageBinder = KFImage.ImageBinder
+    typealias Context = KFImage.Context
+}
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+public struct KFAnimatedImage: View {
+    
+    var context: Context
+
+    /// Creates a Kingfisher compatible image view to load image from the given `Source`.
+    /// - Parameters:
+    ///   - source: The image `Source` defining where to load the target image.
+    ///   - isLoaded: Whether the image is loaded or not. This provides a way to inspect the internal loading
+    ///               state. `true` if the image is loaded successfully. Otherwise, `false`. Do not set the
+    ///               wrapped value from outside.
+    public init(source: Source?, isLoaded: Binding<Bool> = .constant(false)) {
+        let binder = ImageBinder(source: source, isLoaded: isLoaded)
+        self.init(binder: binder)
+    }
+
+    /// Creates a Kingfisher compatible image view to load image from the given `URL`.
+    /// - Parameters:
+    ///   - source: The image `Source` defining where to load the target image.
+    ///   - isLoaded: Whether the image is loaded or not. This provides a way to inspect the internal loading
+    ///               state. `true` if the image is loaded successfully. Otherwise, `false`. Do not set the
+    ///               wrapped value from outside.
+    public init(_ url: URL?, isLoaded: Binding<Bool> = .constant(false)) {
+        self.init(source: url?.convertToSource(), isLoaded: isLoaded)
+    }
+
+    init(binder: ImageBinder) {
+        self.context = Context(binder: binder)
+    }
+    
+    public var body: some View {
+        KFAnimatedImageRender(context)
+            .id(context.binder)
+    }
+    
+    /// Starts the loading process of `self` immediately.
+    ///
+    /// By default, a `KFAnimatedImage` 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.
+    ///
+    /// - Returns: The `Self` value with changes applied.
+    public func loadImmediately(_ start: Bool = true) -> KFAnimatedImage {
+        if start {
+            context.binder.start()
+        }
+        return self
+    }
+    
+}
+
+// MARK: - Image compatibility.
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension KFAnimatedImage {
+
+    /// Configures current image with a `block`. This block will be lazily applied when creating the final `Image`.
+    /// - Parameter block: The block applies to loaded image.
+    /// - Returns: A `KFAnimatedImage` view that configures internal `Image` with `block`.
+    public func configure(_ block: @escaping (Image) -> Image) -> KFAnimatedImage {
+        var result = self
+        result.context.configurations.append(block)
+        return result
+    }
+
+    public func resizable(
+        capInsets: EdgeInsets = EdgeInsets(),
+        resizingMode: Image.ResizingMode = .stretch) -> KFAnimatedImage
+    {
+        configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) }
+    }
+
+    public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFAnimatedImage {
+        configure { $0.renderingMode(renderingMode) }
+    }
+
+    public func interpolation(_ interpolation: Image.Interpolation) -> KFAnimatedImage {
+        configure { $0.interpolation(interpolation) }
+    }
+
+    public func antialiased(_ isAntialiased: Bool) -> KFAnimatedImage {
+        configure { $0.antialiased(isAntialiased) }
+    }
+}
+
+/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`.
+/// Declaring a `KFAnimatedImage` in a `View`'s body to trigger loading from the given `Source`.
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+struct KFAnimatedImageRender: View {
+    /// An image binder that manages loading and cancelling image related task.
+    @ObservedObject var binder: KFAnimatedImage.ImageBinder
+
+    // Acts as a placeholder when loading an image.
+    var placeholder: AnyView?
+
+    // Whether the download task should be cancelled when the view disappears.
+    let cancelOnDisappear: Bool
+
+    init(_ context: KFAnimatedImage.Context) {
+        self.binder = context.binder
+        self.placeholder = context.placeholder
+        self.cancelOnDisappear = context.cancelOnDisappear
+    }
+    
+    /// Declares the content and behavior of this view.
+    @ViewBuilder
+    var body: some View {
+        if let image = binder.loadedImage {
+            KFAnimatedImageViewRepresenter(image: image)
+                .opacity(binder.loaded ? 1.0 : 0.0)
+        } else {
+            Group {
+                if placeholder != nil {
+                    placeholder
+                } else {
+                    Color.clear
+                }
+            }
+            .onAppear { [weak binder = self.binder] in
+                guard let binder = binder else {
+                    return
+                }
+                if !binder.loadingOrSucceeded {
+                    binder.start()
+                }
+            }
+            .onDisappear { [weak binder = self.binder] in
+                guard let binder = binder else {
+                    return
+                }
+                if self.cancelOnDisappear {
+                    binder.cancel()
+                }
+            }
+        }
+    }
+}
+
+/// A wrapped `UIViewRepresentable` of `AnimatedImageView`
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+struct KFAnimatedImageViewRepresenter: UIViewRepresentable {
+    
+    var image: KFCrossPlatformImage?
+    
+    func makeUIView(context: Context) -> AnimatedImageView {
+        let view = AnimatedImageView()
+        view.image = image
+        return view
+    }
+    
+    func updateUIView(_ uiView: AnimatedImageView, context: Context) {
+        uiView.image = image
+    }
+    
+}
+
+#if DEBUG
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+struct KFAnimatedImage_Previews : PreviewProvider {
+    static var previews: some View {
+        Group {
+            KFAnimatedImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif")!))
+                .onSuccess { r in
+                    print(r)
+                }
+                .resizable()
+                .aspectRatio(contentMode: .fit)
+                .padding()
+        }
+    }
+}
+#endif
+
+#endif

+ 146 - 0
Sources/SwiftUI/KFAnimatedImageOptions.swift

@@ -0,0 +1,146 @@
+//
+//  KFAnimatedImageOptions.swift
+//  Kingfisher
+//
+//  Created by wangxingbin on 2021/4/30.
+//
+//  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>
+//
+//  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(SwiftUI) && canImport(Combine)
+import SwiftUI
+
+// MARK: - KFAnimatedImage creating.
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension KFAnimatedImage {
+
+    /// Creates a `KFAnimatedImage` for a given `Source`.
+    /// - Parameters:
+    ///   - source: The `Source` object defines data information from network or a data provider.
+    ///   - isLoaded: Whether the image is loaded or not. This provides a way to inspect the internal loading
+    ///               state. `true` if the image is loaded successfully. Otherwise, `false`. Do not set the
+    ///               wrapped value from outside.
+    /// - Returns: A `KFAnimatedImage` for future configuration or embedding to a `SwiftUI.View`.
+    public static func source(
+        _ source: Source?, isLoaded: Binding<Bool> = .constant(false)
+    ) -> KFAnimatedImage
+    {
+        KFAnimatedImage(source: source, isLoaded: isLoaded)
+    }
+
+    /// Creates a `KFAnimatedImage` for a given `Resource`.
+    /// - Parameters:
+    ///   - source: The `Resource` object defines data information like key or URL.
+    ///   - isLoaded: Whether the image is loaded or not. This provides a way to inspect the internal loading
+    ///               state. `true` if the image is loaded successfully. Otherwise, `false`. Do not set the
+    ///               wrapped value from outside.
+    /// - Returns: A `KFAnimatedImage` for future configuration or embedding to a `SwiftUI.View`.
+    public static func resource(
+        _ resource: Resource?, isLoaded: Binding<Bool> = .constant(false)
+    ) -> KFAnimatedImage
+    {
+        source(resource?.convertToSource(), isLoaded: isLoaded)
+    }
+
+    /// Creates a `KFAnimatedImage` for a given `URL`.
+    /// - 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.
+    ///   - isLoaded: Whether the image is loaded or not. This provides a way to inspect the internal loading
+    ///               state. `true` if the image is loaded successfully. Otherwise, `false`. Do not set the
+    ///               wrapped value from outside.
+    /// - Returns: A `KFAnimatedImage` for future configuration or embedding to a `SwiftUI.View`.
+    public static func url(
+        _ url: URL?, cacheKey: String? = nil, isLoaded: Binding<Bool> = .constant(false)
+    ) -> KFAnimatedImage
+    {
+        source(url?.convertToSource(overrideCacheKey: cacheKey), isLoaded: isLoaded)
+    }
+
+    /// Creates a `KFAnimatedImage` for a given `ImageDataProvider`.
+    /// - Parameters:
+    ///   - provider: The `ImageDataProvider` object contains information about the data.
+    ///   - isLoaded: Whether the image is loaded or not. This provides a way to inspect the internal loading
+    ///               state. `true` if the image is loaded successfully. Otherwise, `false`. Do not set the
+    ///               wrapped value from outside.
+    /// - Returns: A `KFAnimatedImage` for future configuration or embedding to a `SwiftUI.View`.
+    public static func dataProvider(
+        _ provider: ImageDataProvider?, isLoaded: Binding<Bool> = .constant(false)
+    ) -> KFAnimatedImage
+    {
+        source(provider?.convertToSource(), isLoaded: isLoaded)
+    }
+
+    /// 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.
+    ///   - isLoaded: Whether the image is loaded or not. This provides a way to inspect the internal loading
+    ///               state. `true` if the image is loaded successfully. Otherwise, `false`. Do not set the
+    ///               wrapped value from outside.
+    /// - Returns: A `KFAnimatedImage` for future configuration or embedding to a `SwiftUI.View`.
+    public static func data(
+        _ data: Data?, cacheKey: String, isLoaded: Binding<Bool> = .constant(false)
+    ) -> KFAnimatedImage
+    {
+        if let data = data {
+            return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey), isLoaded: isLoaded)
+        } else {
+            return dataProvider(nil, isLoaded: isLoaded)
+        }
+    }
+}
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension KFAnimatedImage {
+    /// Sets a placeholder `View` which shows when loading the image.
+    /// - Parameter content: A view that describes the placeholder.
+    /// - Returns: A `KFAnimatedImage` view that contains `content` as its placeholder.
+    public func placeholder<Content: View>(@ViewBuilder _ content: () -> Content) -> KFAnimatedImage {
+        let v = content()
+        var result = self
+        result.context.placeholder = AnyView(v)
+        return result
+    }
+
+    /// Sets cancelling the download task bound to `self` when the view disappearing.
+    /// - Parameter flag: Whether cancel the task or not.
+    /// - Returns: A `KFAnimatedImage` view that cancels downloading task when disappears.
+    public func cancelOnDisappear(_ flag: Bool) -> KFAnimatedImage {
+        var result = self
+        result.context.cancelOnDisappear = flag
+        return result
+    }
+
+    /// Sets a fade transition for the image task.
+    /// - Parameter duration: The duration of the fade transition.
+    /// - Returns: A `KFAnimatedImage` 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 `KFAnimatedImage`.
+    public func fade(duration: TimeInterval) -> KFAnimatedImage {
+        context.binder.options.transition = .fade(duration)
+        return self
+    }
+}
+#endif