Kaynağa Gözat

Refactor to share code between KFImage and KFAnimatedImage

onevcat 4 yıl önce
ebeveyn
işleme
eab7b62f44

+ 1 - 8
Demo/Demo/Kingfisher-Demo/SwiftUIViews/AnimatedImageDemo.swift

@@ -31,9 +31,7 @@ import Kingfisher
 struct AnimatedImageDemo: View {
     
     @State private var index = 1
-    
-    @State private var blackWhite = false
-    
+        
     var url: URL {
         ImageLoader.gifImageURLs[index - 1]
     }
@@ -42,7 +40,6 @@ struct AnimatedImageDemo: View {
         VStack {
             KFAnimatedImage(url)
                 .cacheOriginalImage()
-                .setProcessor(blackWhite ? BlackWhiteProcessor() : DefaultImageProcessor())
                 .onSuccess { r in
                     print("suc: \(r)")
                 }
@@ -55,7 +52,6 @@ struct AnimatedImageDemo: View {
                 }
                 .fade(duration: 1)
                 .forceTransition()
-                .resizable()
                 .frame(width: 300, height: 300)
                 .cornerRadius(20)
                 .shadow(radius: 5)
@@ -64,9 +60,6 @@ struct AnimatedImageDemo: View {
             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)
     }
     

+ 2 - 1
Demo/Demo/Kingfisher-Demo/SwiftUIViews/MainView.swift

@@ -2,7 +2,7 @@
 //  MainView.swift
 //  Kingfisher
 //
-//  Created by jp20028 on 2019/08/07.
+//  Created by onevcat on 2019/08/07.
 //
 //  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
 //
@@ -47,6 +47,7 @@ struct MainView: View {
                 NavigationLink(destination: LazyVStackDemo()) { Text("Stack") }
                 NavigationLink(destination: GridDemo()) { Text("Grid") }
             }
+            NavigationLink(destination: AnimatedImageDemo()) { Text("Animated Image") }
         }.navigationBarTitle(Text("SwiftUI Sample"))
     }
 }

+ 1 - 1
Demo/Demo/Kingfisher-Demo/ViewControllers/AutoSizingTableViewController.swift

@@ -2,7 +2,7 @@
 //  AutoSizingTableViewController.swift
 //  Kingfisher
 //
-//  Created by JP20028 on 2021/03/15.
+//  Created by onevcat on 2021/03/15.
 //
 //  Copyright (c) 2021 Wei Wang <onevcat@gmail.com>
 //

+ 9 - 1
Kingfisher.xcodeproj/project.pbxproj

@@ -15,6 +15,8 @@
 		4B46CC6921744AC500D90C4A /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC6821744AC500D90C4A /* DiskStorage.swift */; };
 		4B8351C8217066580081EED8 /* StubHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8351C7217066580081EED8 /* StubHelpers.swift */; };
 		4B8351CC217084660081EED8 /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8351CB217084660081EED8 /* Runtime.swift */; };
+		4B88CEB02646C056009EBB41 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */; };
+		4B88CEB22646C653009EBB41 /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */; };
 		4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */; };
 		4B8E291C216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */; };
 		4BA3BF1E228BCDD100909201 /* DataReceivingSideEffectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */; };
@@ -140,6 +142,8 @@
 		4B46CC6821744AC500D90C4A /* DiskStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskStorage.swift; sourceTree = "<group>"; };
 		4B8351C7217066580081EED8 /* StubHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubHelpers.swift; sourceTree = "<group>"; };
 		4B8351CB217084660081EED8 /* Runtime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Runtime.swift; sourceTree = "<group>"; };
+		4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageProtocol.swift; sourceTree = "<group>"; };
+		4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageRenderer.swift; sourceTree = "<group>"; };
 		4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloaderDelegate.swift; sourceTree = "<group>"; };
 		4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationChallengeResponsable.swift; sourceTree = "<group>"; };
 		4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataReceivingSideEffectTests.swift; sourceTree = "<group>"; };
@@ -657,9 +661,11 @@
 			children = (
 				D1F7607523097532000C5269 /* ImageBinder.swift */,
 				D1F7607623097532000C5269 /* KFImage.swift */,
-				D1889533258F7648003B73BE /* KFImageOptions.swift */,
 				07292244263B02F00089E810 /* KFAnimatedImage.swift */,
+				4B88CEB12646C653009EBB41 /* KFImageRenderer.swift */,
+				D1889533258F7648003B73BE /* KFImageOptions.swift */,
 				07292248263B125E0089E810 /* KFAnimatedImageOptions.swift */,
+				4B88CEAF2646C056009EBB41 /* KFImageProtocol.swift */,
 			);
 			path = SwiftUI;
 			sourceTree = "<group>";
@@ -784,6 +790,7 @@
 				D12AB6CC215D2BB50013BA68 /* ImageModifier.swift in Sources */,
 				D12AB718215D2BB50013BA68 /* CacheSerializer.swift in Sources */,
 				D1E56445219B16330057AAE3 /* ImageDataProvider.swift in Sources */,
+				4B88CEB22646C653009EBB41 /* KFImageRenderer.swift in Sources */,
 				D12AB730215D2BB50013BA68 /* AnimatedImageView.swift in Sources */,
 				4B46CC64217449E000D90C4A /* Storage.swift in Sources */,
 				D12AB6E4215D2BB50013BA68 /* Placeholder.swift in Sources */,
@@ -818,6 +825,7 @@
 				D12EB83C24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift in Sources */,
 				D1889534258F7649003B73BE /* KFImageOptions.swift in Sources */,
 				D12AB714215D2BB50013BA68 /* ImageCache.swift in Sources */,
+				4B88CEB02646C056009EBB41 /* KFImageProtocol.swift in Sources */,
 				D12AB6D0215D2BB50013BA68 /* ImagePrefetcher.swift in Sources */,
 				D12AB6F4215D2BB50013BA68 /* ImageView+Kingfisher.swift in Sources */,
 				D12AB6FC215D2BB50013BA68 /* UIButton+Kingfisher.swift in Sources */,

+ 14 - 63
Sources/SwiftUI/KFAnimatedImage.swift

@@ -28,15 +28,14 @@
 import SwiftUI
 
 @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
-internal extension KFAnimatedImage {
+public struct KFAnimatedImage: KFImageProtocol {
+    
+    public typealias Context = KFImage.Context
     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
+    public typealias HoldingView = KFAnimatedImageViewRepresenter
+    
+    public internal (set) var context: Context<HoldingView>
 
     /// Creates a Kingfisher compatible image view to load image from the given `Source`.
     /// - Parameters:
@@ -64,58 +63,9 @@ public struct KFAnimatedImage: View {
     }
     
     public var body: some View {
-        KFAnimatedImageRender(context)
+        KFImageRenderer<KFAnimatedImageViewRepresenter>(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`.
@@ -131,7 +81,7 @@ struct KFAnimatedImageRender: View {
     // Whether the download task should be cancelled when the view disappears.
     let cancelOnDisappear: Bool
 
-    init(_ context: KFAnimatedImage.Context) {
+    init(_ context: KFAnimatedImage.Context<Image>) {
         self.binder = context.binder
         self.placeholder = context.placeholder
         self.cancelOnDisappear = context.cancelOnDisappear
@@ -173,17 +123,20 @@ struct KFAnimatedImageRender: View {
 
 /// A wrapped `UIViewRepresentable` of `AnimatedImageView`
 @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
-struct KFAnimatedImageViewRepresenter: UIViewRepresentable {
+public struct KFAnimatedImageViewRepresenter: UIViewRepresentable, KFImageHoldingView {
+    public static func created(from image: KFCrossPlatformImage) -> KFAnimatedImageViewRepresenter {
+        KFAnimatedImageViewRepresenter(image: image)
+    }
     
     var image: KFCrossPlatformImage?
     
-    func makeUIView(context: Context) -> AnimatedImageView {
+    public func makeUIView(context: Context) -> AnimatedImageView {
         let view = AnimatedImageView()
         view.image = image
         return view
     }
     
-    func updateUIView(_ uiView: AnimatedImageView, context: Context) {
+    public func updateUIView(_ uiView: AnimatedImageView, context: Context) {
         uiView.image = image
     }
     
@@ -198,8 +151,6 @@ struct KFAnimatedImage_Previews : PreviewProvider {
                 .onSuccess { r in
                     print(r)
                 }
-                .resizable()
-                .aspectRatio(contentMode: .fit)
                 .padding()
         }
     }

+ 42 - 172
Sources/SwiftUI/KFImage.swift

@@ -29,52 +29,11 @@ import Combine
 import SwiftUI
 
 @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
-extension Image {
-    // Creates an Image with either UIImage or NSImage.
-    init(crossPlatformImage: KFCrossPlatformImage) {
-        #if canImport(UIKit)
-        self.init(uiImage: crossPlatformImage)
-        #elseif canImport(AppKit)
-        self.init(nsImage: crossPlatformImage)
-        #endif
-    }
-}
-
-@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
-public struct KFImage: View {
+public struct KFImage: KFImageProtocol {
+    
+    public typealias HoldingView = Image
 
-    var context: Context
-
-    /// Creates a Kingfisher compatible image view to load image from the given `Source`.
-    /// - Parameter source: The image `Source` defining where to load the target image.
-    /// - Parameter options: The options should be applied when loading the image.
-    ///                      Some UIKit related options (such as `ImageTransition.flip`) are not supported.
-    /// - Parameter 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.
-    /// - Deprecated: Some options are not available in SwiftUI yet. Use `KFImage(source:isLoaded:)` to create a
-    ///               `KFImage` and configure the options through modifier instead. See methods of `KFOptionSetter`
-    ///               for more.
-    @available(*, deprecated, message: "Some options are not available in SwiftUI yet. Use `KFImage(source:isLoaded:)` to create a `KFImage` and configure the options through modifier instead.")
-    public init(source: Source?, options: KingfisherOptionsInfo? = nil, isLoaded: Binding<Bool> = .constant(false)) {
-        let binder = KFImage.ImageBinder(source: source, options: options, isLoaded: isLoaded)
-        self.init(binder: binder)
-    }
-
-    /// Creates a Kingfisher compatible image view to load image from the given `URL`.
-    /// - Parameter url: The image URL from where to load the target image.
-    /// - Parameter options: The options should be applied when loading the image.
-    ///                      Some UIKit related options (such as `ImageTransition.flip`) are not supported.
-    /// - Parameter 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.
-    /// - Deprecated: Some options are not available in SwiftUI yet. Use `KFImage(_:isLoaded:)` to create a
-    ///               `KFImage` and configure the options through modifier instead. See methods of `KFOptionSetter`
-    ///               for more.
-    @available(*, deprecated, message: "Some options are not available in SwiftUI yet. Use `KFImage(_:isLoaded:)` to create a `KFImage` and configure the options through modifier instead.")
-    init(_ url: URL?, options: KingfisherOptionsInfo? = nil, isLoaded: Binding<Bool> = .constant(false)) {
-        self.init(source: url?.convertToSource(), options: options, isLoaded: isLoaded)
-    }
+    public internal (set) var context: Context<HoldingView>
 
     /// Creates a Kingfisher compatible image view to load image from the given `Source`.
     /// - Parameters:
@@ -98,35 +57,15 @@ public struct KFImage: View {
     }
 
     init(binder: ImageBinder) {
-        self.context = Context(binder: binder)
-    }
-
-    public var body: some View {
-        KFImageRenderer(context)
-            .id(context.binder)
-    }
-
-    /// 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.
-    ///
-    /// - Returns: The `Self` value with changes applied.
-    public func loadImmediately(_ start: Bool = true) -> KFImage {
-        if start {
-            context.binder.start()
-        }
-        return self
+        self.context = Context<HoldingView>(binder: binder)
     }
 }
 
 @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
 extension KFImage {
-    struct Context {
+    public struct Context<HoldingView: KFImageHoldingView> {
         var binder: ImageBinder
-        var configurations: [(Image) -> Image] = []
+        var configurations: [(HoldingView) -> HoldingView] = []
         var cancelOnDisappear: Bool = false
         var placeholder: AnyView? = nil
 
@@ -136,110 +75,6 @@ extension KFImage {
     }
 }
 
-/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`.
-/// Declaring a `KFImage` 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 KFImageRenderer: View {
-
-    /// An image binder that manages loading and cancelling image related task.
-    @ObservedObject var binder: KFImage.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
-
-    // Configurations should be performed on the image.
-    let configurations: [(Image) -> Image]
-
-    init(_ context: KFImage.Context) {
-        self.binder = context.binder
-        self.configurations = context.configurations
-        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 {
-            configurations
-                .reduce(imageFromResult(image)) {
-                    current, config in config(current)
-                }
-                .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()
-                }
-            }
-        }
-    }
-
-    private func imageFromResult(_ resultImage: KFCrossPlatformImage) -> Image {
-        if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
-            return Image(crossPlatformImage: resultImage)
-        } else {
-            #if canImport(UIKit)
-            // The CG image is used to solve #1395
-            // It should be not necessary if SwiftUI.Image can handle resizing correctly when created
-            // by `Image.init(uiImage:)`. (The orientation information should be already contained in
-            // a `UIImage`)
-            // https://github.com/onevcat/Kingfisher/issues/1395
-            //
-            // This issue happens on iOS 13 and was fixed by Apple from iOS 14.
-            if let cgImage = resultImage.cgImage {
-                return Image(decorative: cgImage, scale: resultImage.scale, orientation: resultImage.imageOrientation.toSwiftUI())
-            } else {
-                return Image(crossPlatformImage: resultImage)
-            }
-            #else
-            return Image(crossPlatformImage: resultImage)
-            #endif
-
-        }
-    }
-}
-
-#if canImport(UIKit)
-@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
-extension UIImage.Orientation {
-    func toSwiftUI() -> Image.Orientation {
-        switch self {
-        case .down: return .down
-        case .up: return .up
-        case .left: return .left
-        case .right: return .right
-        case .upMirrored: return .upMirrored
-        case .downMirrored: return .downMirrored
-        case .leftMirrored: return .leftMirrored
-        case .rightMirrored: return .rightMirrored
-        @unknown default: return .up
-        }
-    }
-}
-#endif
-
 // MARK: - Image compatibility.
 @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
 extension KFImage {
@@ -273,6 +108,41 @@ extension KFImage {
     }
 }
 
+// MARK: - Deprecated
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension KFImage {
+    /// Creates a Kingfisher compatible image view to load image from the given `Source`.
+    /// - Parameter source: The image `Source` defining where to load the target image.
+    /// - Parameter options: The options should be applied when loading the image.
+    ///                      Some UIKit related options (such as `ImageTransition.flip`) are not supported.
+    /// - Parameter 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.
+    /// - Deprecated: Some options are not available in SwiftUI yet. Use `KFImage(source:isLoaded:)` to create a
+    ///               `KFImage` and configure the options through modifier instead. See methods of `KFOptionSetter`
+    ///               for more.
+    @available(*, deprecated, message: "Some options are not available in SwiftUI yet. Use `KFImage(source:isLoaded:)` to create a `KFImage` and configure the options through modifier instead.")
+    public init(source: Source?, options: KingfisherOptionsInfo? = nil, isLoaded: Binding<Bool> = .constant(false)) {
+        let binder = KFImage.ImageBinder(source: source, options: options, isLoaded: isLoaded)
+        self.init(binder: binder)
+    }
+
+    /// Creates a Kingfisher compatible image view to load image from the given `URL`.
+    /// - Parameter url: The image URL from where to load the target image.
+    /// - Parameter options: The options should be applied when loading the image.
+    ///                      Some UIKit related options (such as `ImageTransition.flip`) are not supported.
+    /// - Parameter 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.
+    /// - Deprecated: Some options are not available in SwiftUI yet. Use `KFImage(_:isLoaded:)` to create a
+    ///               `KFImage` and configure the options through modifier instead. See methods of `KFOptionSetter`
+    ///               for more.
+    @available(*, deprecated, message: "Some options are not available in SwiftUI yet. Use `KFImage(_:isLoaded:)` to create a `KFImage` and configure the options through modifier instead.")
+    init(_ url: URL?, options: KingfisherOptionsInfo? = nil, isLoaded: Binding<Bool> = .constant(false)) {
+        self.init(source: url?.convertToSource(), options: options, isLoaded: isLoaded)
+    }
+}
+
 #if DEBUG
 @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
 struct KFImage_Previews : PreviewProvider {

+ 62 - 0
Sources/SwiftUI/KFImageProtocol.swift

@@ -0,0 +1,62 @@
+//
+//  KFImageProtocol.swift
+//  Kingfisher
+//
+//  Created by onevcat on 2021/05/08.
+//
+//  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
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+public protocol KFImageProtocol: View {
+    associatedtype HoldingView: KFImageHoldingView
+    var context: KFImage.Context<HoldingView> { get }
+}
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension KFImageProtocol {
+    public var body: some View {
+        KFImageRenderer<HoldingView>(context)
+            .id(context.binder)
+    }
+    
+    /// 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.
+    ///
+    /// - Returns: The `Self` value with changes applied.
+    public func loadImmediately(_ start: Bool = true) -> Self {
+        if start {
+            context.binder.start()
+        }
+        return self
+    }
+}
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+public protocol KFImageHoldingView: View {
+    static func created(from image: KFCrossPlatformImage) -> Self
+}
+

+ 146 - 0
Sources/SwiftUI/KFImageRenderer.swift

@@ -0,0 +1,146 @@
+//
+//  KFImageRenderer.swift
+//  Kingfisher
+//
+//  Created by onevcat on 2021/05/08.
+//
+//  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
+
+/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`.
+/// Declaring a `KFImage` 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 KFImageRenderer<HoldingView> : View where HoldingView: KFImageHoldingView {
+    
+    /// An image binder that manages loading and cancelling image related task.
+    @ObservedObject var binder: KFImage.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
+
+    // Configurations should be performed on the image.
+    let configurations: [(HoldingView) -> HoldingView]
+
+    init(_ context: KFImage.Context<HoldingView>) {
+        self.binder = context.binder
+        self.configurations = context.configurations
+        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 {
+            configurations
+                .reduce(HoldingView.created(from: image)) {
+                    current, config in config(current)
+                }
+                .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()
+                }
+            }
+        }
+    }
+}
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension Image: KFImageHoldingView {
+    public static func created(from image: KFCrossPlatformImage) -> Image {
+        if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
+            return Image(crossPlatformImage: image)
+        } else {
+            #if canImport(UIKit)
+            // The CG image is used to solve #1395
+            // It should be not necessary if SwiftUI.Image can handle resizing correctly when created
+            // by `Image.init(uiImage:)`. (The orientation information should be already contained in
+            // a `UIImage`)
+            // https://github.com/onevcat/Kingfisher/issues/1395
+            //
+            // This issue happens on iOS 13 and was fixed by Apple from iOS 14.
+            if let cgImage = image.cgImage {
+                return Image(decorative: cgImage, scale: image.scale, orientation: image.imageOrientation.toSwiftUI())
+            } else {
+                return Image(crossPlatformImage: image)
+            }
+            #else
+            return Image(crossPlatformImage: image)
+            #endif
+
+        }
+    }
+}
+
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension Image {
+    // Creates an Image with either UIImage or NSImage.
+    init(crossPlatformImage: KFCrossPlatformImage) {
+        #if canImport(UIKit)
+        self.init(uiImage: crossPlatformImage)
+        #elseif canImport(AppKit)
+        self.init(nsImage: crossPlatformImage)
+        #endif
+    }
+}
+
+#if canImport(UIKit)
+@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
+extension UIImage.Orientation {
+    func toSwiftUI() -> Image.Orientation {
+        switch self {
+        case .down: return .down
+        case .up: return .up
+        case .left: return .left
+        case .right: return .right
+        case .upMirrored: return .upMirrored
+        case .downMirrored: return .downMirrored
+        case .leftMirrored: return .leftMirrored
+        case .rightMirrored: return .rightMirrored
+        @unknown default: return .up
+        }
+    }
+}
+#endif