Просмотр исходного кода

Merge pull request #1726 from onevcat/fix/simple-model

Give a simpler model structure for `@StateObject` based SwiftUI support
Wei Wang 4 лет назад
Родитель
Сommit
d066206835

+ 2 - 3
Demo/Demo/Kingfisher-Demo/SwiftUIViews/AnimatedImageDemo.swift

@@ -46,9 +46,8 @@ struct AnimatedImageDemo: View {
                 .onFailure { e in
                 .onFailure { e in
                     print("err: \(e)")
                     print("err: \(e)")
                 }
                 }
-                .placeholder {
-                    Image(systemName: "arrow.2.circlepath.circle")
-                        .font(.largeTitle)
+                .placeholder { p in
+                    ProgressView(p)
                 }
                 }
                 .fade(duration: 1)
                 .fade(duration: 1)
                 .forceTransition()
                 .forceTransition()

+ 1 - 3
Demo/Demo/Kingfisher-Demo/SwiftUIViews/ListDemo.swift

@@ -43,8 +43,6 @@ struct ListDemo : View {
 @available(iOS 14.0, *)
 @available(iOS 14.0, *)
 struct ImageCell: View {
 struct ImageCell: View {
 
 
-    @State var done = false
-
     var alreadyCached: Bool {
     var alreadyCached: Bool {
         ImageCache.default.isCached(forKey: url.absoluteString)
         ImageCache.default.isCached(forKey: url.absoluteString)
     }
     }
@@ -57,7 +55,7 @@ struct ImageCell: View {
     var body: some View {
     var body: some View {
         HStack(alignment: .center) {
         HStack(alignment: .center) {
             Spacer()
             Spacer()
-            KFImage.url(url, isLoaded: $done)
+            KFImage.url(url)
                 .resizable()
                 .resizable()
                 .onSuccess { r in
                 .onSuccess { r in
                     print("Success: \(self.index) - \(r.cacheType)")
                     print("Success: \(self.index) - \(r.cacheType)")

+ 2 - 4
Demo/Demo/Kingfisher-Demo/SwiftUIViews/SingleViewDemo.swift

@@ -31,7 +31,6 @@ import SwiftUI
 struct SingleViewDemo : View {
 struct SingleViewDemo : View {
 
 
     @State private var index = 1
     @State private var index = 1
-
     @State private var blackWhite = false
     @State private var blackWhite = false
 
 
     var url: URL {
     var url: URL {
@@ -49,9 +48,8 @@ struct SingleViewDemo : View {
                 .onFailure { e in
                 .onFailure { e in
                     print("err: \(e)")
                     print("err: \(e)")
                 }
                 }
-                .placeholder {
-                    Image(systemName: "arrow.2.circlepath.circle")
-                        .font(.largeTitle)
+                .placeholder { progress in
+                    ProgressView(progress)
                 }
                 }
                 .fade(duration: 1)
                 .fade(duration: 1)
                 .forceTransition()
                 .forceTransition()

+ 0 - 1
Demo/Demo/Kingfisher-Demo/SwiftUIViews/SizingAnimationDemo.swift

@@ -35,7 +35,6 @@ struct SizingAnimationDemo: View {
     var body: some View {
     var body: some View {
         VStack {
         VStack {
             KFImage(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher-1.jpg")!)
             KFImage(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher-1.jpg")!)
-                .loadImmediately()
                 .resizable()
                 .resizable()
                 .aspectRatio(contentMode: .fill)
                 .aspectRatio(contentMode: .fill)
                 .frame(width: imageSize, height: imageSize)
                 .frame(width: imageSize, height: imageSize)

+ 19 - 76
Sources/SwiftUI/ImageBinder.swift

@@ -34,9 +34,8 @@ extension KFImage {
     /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs
     /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs
     /// image downloading and progress reporting based on `KingfisherManager`.
     /// image downloading and progress reporting based on `KingfisherManager`.
     class ImageBinder: ObservableObject {
     class ImageBinder: ObservableObject {
-
-        let source: Source?
-        var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions)
+        
+        init() {}
 
 
         var downloadTask: DownloadTask?
         var downloadTask: DownloadTask?
 
 
@@ -44,56 +43,29 @@ extension KFImage {
             return downloadTask != nil || loadedImage != nil
             return downloadTask != nil || loadedImage != nil
         }
         }
 
 
-        let onFailureDelegate = Delegate<KingfisherError, Void>()
-        let onSuccessDelegate = Delegate<RetrieveImageResult, Void>()
-        let onProgressDelegate = Delegate<(Int64, Int64), Void>()
-
-        var isLoaded: Binding<Bool>
-
         @Published var loaded = false
         @Published var loaded = false
         @Published var loadedImage: KFCrossPlatformImage? = nil
         @Published var loadedImage: KFCrossPlatformImage? = nil
+        @Published var progress: Progress = .init()
 
 
-        @available(*, deprecated, message: "The `options` version is deprecated And will be removed soon.")
-        init(source: Source?, options: KingfisherOptionsInfo? = nil, isLoaded: Binding<Bool>) {
-            self.source = source
-            // The refreshing of `KFImage` would happen much more frequently then an `UIImageView`, even as a
-            // "side-effect". To prevent unintended flickering, add `.loadDiskFileSynchronously` as a default.
-            self.options = KingfisherParsedOptionsInfo(
-                KingfisherManager.shared.defaultOptions +
-                (options ?? []) +
-                [.loadDiskFileSynchronously]
-            )
-            self.isLoaded = isLoaded
-        }
-
-        init(source: Source?, isLoaded: Binding<Bool>) {
-            self.source = source
-            // The refreshing of `KFImage` would happen much more frequently then an `UIImageView`, even as a
-            // "side-effect". To prevent unintended flickering, add `.loadDiskFileSynchronously` as a default.
-            self.options = KingfisherParsedOptionsInfo(
-                KingfisherManager.shared.defaultOptions +
-                [.loadDiskFileSynchronously]
-            )
-            self.isLoaded = isLoaded
-        }
-
-        func start() {
+        func start<HoldingView: KFImageHoldingView>(context: Context<HoldingView>) {
 
 
             guard !loadingOrSucceeded else { return }
             guard !loadingOrSucceeded else { return }
 
 
-            guard let source = source else {
+            guard let source = context.source else {
                 CallbackQueue.mainCurrentOrAsync.execute {
                 CallbackQueue.mainCurrentOrAsync.execute {
-                    self.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource))
+                    context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource))
                 }
                 }
                 return
                 return
             }
             }
 
 
+            progress = .init()
             downloadTask = KingfisherManager.shared
             downloadTask = KingfisherManager.shared
                 .retrieveImage(
                 .retrieveImage(
                     with: source,
                     with: source,
-                    options: options,
+                    options: context.options,
                     progressBlock: { size, total in
                     progressBlock: { size, total in
-                        self.onProgressDelegate.call((size, total))
+                        self.updateProgress(downloaded: size, total: total)
+                        context.onProgressDelegate.call((size, total))
                     },
                     },
                     completionHandler: { [weak self] result in
                     completionHandler: { [weak self] result in
 
 
@@ -105,62 +77,33 @@ extension KFImage {
 
 
                             CallbackQueue.mainCurrentOrAsync.execute {
                             CallbackQueue.mainCurrentOrAsync.execute {
                                 self.loadedImage = value.image
                                 self.loadedImage = value.image
-                                self.isLoaded.wrappedValue = true
-                                let animation = self.fadeTransitionDuration(cacheType: value.cacheType)
+                                let animation = context.fadeTransitionDuration(cacheType: value.cacheType)
                                     .map { duration in Animation.linear(duration: duration) }
                                     .map { duration in Animation.linear(duration: duration) }
                                 withAnimation(animation) { self.loaded = true }
                                 withAnimation(animation) { self.loaded = true }
                             }
                             }
 
 
                             CallbackQueue.mainAsync.execute {
                             CallbackQueue.mainAsync.execute {
-                                self.onSuccessDelegate.call(value)
+                                context.onSuccessDelegate.call(value)
                             }
                             }
                         case .failure(let error):
                         case .failure(let error):
                             CallbackQueue.mainAsync.execute {
                             CallbackQueue.mainAsync.execute {
-                                self.onFailureDelegate.call(error)
+                                context.onFailureDelegate.call(error)
                             }
                             }
                         }
                         }
                 })
                 })
         }
         }
+        
+        private func updateProgress(downloaded: Int64, total: Int64) {
+            progress.totalUnitCount = total
+            progress.completedUnitCount = downloaded
+            objectWillChange.send()
+        }
 
 
         /// Cancels the download task if it is in progress.
         /// Cancels the download task if it is in progress.
         func cancel() {
         func cancel() {
             downloadTask?.cancel()
             downloadTask?.cancel()
             downloadTask = nil
             downloadTask = nil
         }
         }
-
-        private func shouldApplyFade(cacheType: CacheType) -> Bool {
-            options.forceTransition || cacheType == .none
-        }
-
-        private func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? {
-            shouldApplyFade(cacheType: cacheType)
-                ? options.transition.fadeDuration
-                : nil
-        }
-    }
-}
-
-@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
-extension KFImage.ImageBinder: Hashable {
-    static func == (lhs: KFImage.ImageBinder, rhs: KFImage.ImageBinder) -> Bool {
-        lhs.source == rhs.source && lhs.options.processor.identifier == rhs.options.processor.identifier
-    }
-
-    func hash(into hasher: inout Hasher) {
-        hasher.combine(source)
-        hasher.combine(options.processor.identifier)
-    }
-}
-
-extension ImageTransition {
-    // Only for fade effect in SwiftUI.
-    fileprivate var fadeDuration: TimeInterval? {
-        switch self {
-        case .fade(let duration):
-            return duration
-        default:
-            return nil
-        }
     }
     }
 }
 }
 #endif
 #endif

+ 50 - 5
Sources/SwiftUI/ImageContext.swift

@@ -30,18 +30,63 @@ import Combine
 
 
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFImage {
 extension KFImage {
-    public struct Context<HoldingView: KFImageHoldingView> {
-        var binder: ImageBinder
+    public class Context<HoldingView: KFImageHoldingView> {
+        let source: Source?
+        var options = KingfisherParsedOptionsInfo(
+            KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously]
+        )
+
         var configurations: [(HoldingView) -> HoldingView] = []
         var configurations: [(HoldingView) -> HoldingView] = []
+        
         var cancelOnDisappear: Bool = false
         var cancelOnDisappear: Bool = false
-        var placeholder: AnyView? = nil
+        var placeholder: ((Progress) -> AnyView)? = nil
+
+        let onFailureDelegate = Delegate<KingfisherError, Void>()
+        let onSuccessDelegate = Delegate<RetrieveImageResult, Void>()
+        let onProgressDelegate = Delegate<(Int64, Int64), Void>()
+        
+        init(source: Source?) {
+            self.source = source
+        }
+        
+        func shouldApplyFade(cacheType: CacheType) -> Bool {
+            options.forceTransition || cacheType == .none
+        }
+
+        func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? {
+            shouldApplyFade(cacheType: cacheType)
+            ? options.transition.fadeDuration
+                : nil
+        }
+    }
+}
 
 
-        init(binder: ImageBinder) {
-            self.binder = binder
+extension ImageTransition {
+    // Only for fade effect in SwiftUI.
+    fileprivate var fadeDuration: TimeInterval? {
+        switch self {
+        case .fade(let duration):
+            return duration
+        default:
+            return nil
         }
         }
     }
     }
 }
 }
 
 
+
+@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
+extension KFImage.Context: Hashable {
+    public static func == (lhs: KFImage.Context<HoldingView>, rhs: KFImage.Context<HoldingView>) -> Bool {
+        lhs.source == rhs.source &&
+        lhs.options.processor.identifier == rhs.options.processor.identifier
+    }
+
+    public func hash(into hasher: inout Hasher) {
+        hasher.combine(source)
+        hasher.combine(options.processor.identifier)
+    }
+}
+
 #if canImport(UIKit) && !os(watchOS)
 #if canImport(UIKit) && !os(watchOS)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFAnimatedImage {
 extension KFAnimatedImage {

+ 3 - 0
Sources/SwiftUI/KFAnimatedImage.swift

@@ -71,6 +71,9 @@ struct KFAnimatedImage_Previews : PreviewProvider {
                 .onSuccess { r in
                 .onSuccess { r in
                     print(r)
                     print(r)
                 }
                 }
+                .placeholder {
+                    ProgressView()
+                }
                 .padding()
                 .padding()
         }
         }
     }
     }

+ 3 - 35
Sources/SwiftUI/KFImage.swift

@@ -67,41 +67,6 @@ extension KFImage {
     }
     }
 }
 }
 
 
-// MARK: - Deprecated
-@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.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
 #if DEBUG
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 struct KFImage_Previews : PreviewProvider {
 struct KFImage_Previews : PreviewProvider {
@@ -111,6 +76,9 @@ struct KFImage_Previews : PreviewProvider {
                 .onSuccess { r in
                 .onSuccess { r in
                     print(r)
                     print(r)
                 }
                 }
+                .placeholder { p in
+                    ProgressView(p)
+                }
                 .resizable()
                 .resizable()
                 .aspectRatio(contentMode: .fit)
                 .aspectRatio(contentMode: .fit)
                 .padding()
                 .padding()

+ 26 - 35
Sources/SwiftUI/KFImageOptions.swift

@@ -35,29 +35,23 @@ extension KFImageProtocol {
     /// Creates a `KFImage` for a given `Source`.
     /// Creates a `KFImage` for a given `Source`.
     /// - Parameters:
     /// - Parameters:
     ///   - source: The `Source` object defines data information from network or a data provider.
     ///   - 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 `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     public static func source(
     public static func source(
-        _ source: Source?, isLoaded: Binding<Bool> = .constant(false)
+        _ source: Source?
     ) -> Self
     ) -> Self
     {
     {
-        Self.init(source: source, isLoaded: isLoaded)
+        Self.init(source: source)
     }
     }
 
 
     /// Creates a `KFImage` for a given `Resource`.
     /// Creates a `KFImage` for a given `Resource`.
     /// - Parameters:
     /// - Parameters:
     ///   - source: The `Resource` object defines data information like key or URL.
     ///   - 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 `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     public static func resource(
     public static func resource(
-        _ resource: Resource?, isLoaded: Binding<Bool> = .constant(false)
+        _ resource: Resource?
     ) -> Self
     ) -> Self
     {
     {
-        source(resource?.convertToSource(), isLoaded: isLoaded)
+        source(resource?.convertToSource())
     }
     }
 
 
     /// Creates a `KFImage` for a given `URL`.
     /// Creates a `KFImage` for a given `URL`.
@@ -65,70 +59,67 @@ extension KFImageProtocol {
     ///   - url: The URL where the image should be downloaded.
     ///   - url: The URL where the image should be downloaded.
     ///   - cacheKey: The key used to store the downloaded image in cache.
     ///   - cacheKey: The key used to store the downloaded image in cache.
     ///               If `nil`, the `absoluteString` of `url` is used as the cache key.
     ///               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 `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     public static func url(
     public static func url(
-        _ url: URL?, cacheKey: String? = nil, isLoaded: Binding<Bool> = .constant(false)
+        _ url: URL?, cacheKey: String? = nil
     ) -> Self
     ) -> Self
     {
     {
-        source(url?.convertToSource(overrideCacheKey: cacheKey), isLoaded: isLoaded)
+        source(url?.convertToSource(overrideCacheKey: cacheKey))
     }
     }
 
 
     /// Creates a `KFImage` for a given `ImageDataProvider`.
     /// Creates a `KFImage` for a given `ImageDataProvider`.
     /// - Parameters:
     /// - Parameters:
     ///   - provider: The `ImageDataProvider` object contains information about the data.
     ///   - 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 `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     public static func dataProvider(
     public static func dataProvider(
-        _ provider: ImageDataProvider?, isLoaded: Binding<Bool> = .constant(false)
+        _ provider: ImageDataProvider?
     ) -> Self
     ) -> Self
     {
     {
-        source(provider?.convertToSource(), isLoaded: isLoaded)
+        source(provider?.convertToSource())
     }
     }
 
 
     /// Creates a builder for some given raw data and a cache key.
     /// Creates a builder for some given raw data and a cache key.
     /// - Parameters:
     /// - Parameters:
     ///   - data: The data object from which the image should be created.
     ///   - data: The data object from which the image should be created.
     ///   - cacheKey: The key used to store the downloaded image in cache.
     ///   - 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 `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
     public static func data(
     public static func data(
-        _ data: Data?, cacheKey: String, isLoaded: Binding<Bool> = .constant(false)
+        _ data: Data?, cacheKey: String
     ) -> Self
     ) -> Self
     {
     {
         if let data = data {
         if let data = data {
-            return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey), isLoaded: isLoaded)
+            return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey))
         } else {
         } else {
-            return dataProvider(nil, isLoaded: isLoaded)
+            return dataProvider(nil)
         }
         }
     }
     }
 }
 }
 
 
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFImageProtocol {
 extension KFImageProtocol {
+    /// Sets a placeholder `View` which shows when loading the image, with a progress parameter as input.
+    /// - Parameter content: A view that describes the placeholder.
+    /// - Returns: A `KFImage` view that contains `content` as its placeholder.
+    public func placeholder<P: View>(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self {
+        context.placeholder = { progress in
+            return AnyView(content(progress))
+        }
+        return self
+    }
+    
     /// Sets a placeholder `View` which shows when loading the image.
     /// Sets a placeholder `View` which shows when loading the image.
     /// - Parameter content: A view that describes the placeholder.
     /// - Parameter content: A view that describes the placeholder.
     /// - Returns: A `KFImage` view that contains `content` as its placeholder.
     /// - Returns: A `KFImage` view that contains `content` as its placeholder.
-    public func placeholder<Content: View>(@ViewBuilder _ content: () -> Content) -> Self {
-        let v = content()
-        var result = self
-        result.context.placeholder = AnyView(v)
-        return result
+    public func placeholder<P: View>(@ViewBuilder _ content: @escaping () -> P) -> Self {
+        placeholder { _ in content() }
     }
     }
 
 
     /// Sets cancelling the download task bound to `self` when the view disappearing.
     /// Sets cancelling the download task bound to `self` when the view disappearing.
     /// - Parameter flag: Whether cancel the task or not.
     /// - Parameter flag: Whether cancel the task or not.
     /// - Returns: A `KFImage` view that cancels downloading task when disappears.
     /// - Returns: A `KFImage` view that cancels downloading task when disappears.
     public func cancelOnDisappear(_ flag: Bool) -> Self {
     public func cancelOnDisappear(_ flag: Bool) -> Self {
-        var result = self
-        result.context.cancelOnDisappear = flag
-        return result
+        context.cancelOnDisappear = flag
+        return self
     }
     }
 
 
     /// Sets a fade transition for the image task.
     /// Sets a fade transition for the image task.
@@ -140,7 +131,7 @@ extension KFImageProtocol {
     /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when
     /// 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 `KFImage`.
     /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KFImage`.
     public func fade(duration: TimeInterval) -> Self {
     public func fade(duration: TimeInterval) -> Self {
-        context.binder.options.transition = .fade(duration)
+        context.options.transition = .fade(duration)
         return self
         return self
     }
     }
 }
 }

+ 15 - 46
Sources/SwiftUI/KFImageProtocol.swift

@@ -38,62 +38,31 @@ public protocol KFImageProtocol: View, KFOptionSetter {
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFImageProtocol {
 extension KFImageProtocol {
     public var body: some View {
     public var body: some View {
-        KFImageRenderer<HoldingView>(
-            binder: context.binder,
-            placeholder: context.placeholder,
-            cancelOnDisappear: context.cancelOnDisappear,
-            configurations: context.configurations
-        )
-            .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
+        KFImageRenderer<HoldingView>(context: context)
+            .id(context)
     }
     }
     
     
     /// Creates a Kingfisher compatible image view to load image from the given `Source`.
     /// Creates a Kingfisher compatible image view to load image from the given `Source`.
     /// - Parameters:
     /// - Parameters:
     ///   - source: The image `Source` defining where to load the target image.
     ///   - 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 = KFImage.ImageBinder(source: source, isLoaded: isLoaded)
-        self.init(binder: binder)
+    public init(source: Source?) {
+        let context = KFImage.Context<HoldingView>(source: source)
+        self.init(context: context)
     }
     }
 
 
     /// Creates a Kingfisher compatible image view to load image from the given `URL`.
     /// Creates a Kingfisher compatible image view to load image from the given `URL`.
     /// - Parameters:
     /// - Parameters:
     ///   - source: The image `Source` defining where to load the target image.
     ///   - 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: KFImage.ImageBinder) {
-        self.init(context: KFImage.Context<HoldingView>(binder: binder))
+    public init(_ url: URL?) {
+        self.init(source: url?.convertToSource())
     }
     }
     
     
     /// Configures current image with a `block`. This block will be lazily applied when creating the final `Image`.
     /// 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.
     /// - Parameter block: The block applies to loaded image.
     /// - Returns: A `KFImage` view that configures internal `Image` with `block`.
     /// - Returns: A `KFImage` view that configures internal `Image` with `block`.
     public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self {
     public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self {
-        var result = self
-        result.context.configurations.append(block)
-        return result
+        context.configurations.append(block)
+        return self
     }
     }
 }
 }
 
 
@@ -105,15 +74,15 @@ public protocol KFImageHoldingView: View {
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFImageProtocol {
 extension KFImageProtocol {
     public var options: KingfisherParsedOptionsInfo {
     public var options: KingfisherParsedOptionsInfo {
-        get { context.binder.options }
-        nonmutating set { context.binder.options = newValue }
+        get { context.options }
+        nonmutating set { context.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 onFailureDelegate: Delegate<KingfisherError, Void> { context.onFailureDelegate }
+    public var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { context.onSuccessDelegate }
+    public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate }
 
 
-    public var delegateObserver: AnyObject { context.binder }
+    public var delegateObserver: AnyObject { context }
 }
 }
 
 
 
 

+ 31 - 40
Sources/SwiftUI/KFImageRenderer.swift

@@ -33,49 +33,40 @@ import Combine
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 struct KFImageRenderer<HoldingView> : View where HoldingView: KFImageHoldingView {
 struct KFImageRenderer<HoldingView> : View where HoldingView: KFImageHoldingView {
     
     
-    /// An image binder that manages loading and cancelling image related task.
-    @StateObject 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]
-
-    /// Declares the content and behavior of this view.
-    @ViewBuilder
+    @StateObject var binder: KFImage.ImageBinder = .init()
+    let context: KFImage.Context<HoldingView>
+    
     var body: some View {
     var body: some View {
-        if let image = binder.loadedImage {
-            configurations
-                .reduce(HoldingView.created(from: image)) {
-                    current, config in config(current)
+        Group {
+            if let image = binder.loadedImage {
+                context.configurations
+                    .reduce(HoldingView.created(from: image)) {
+                        current, config in config(current)
+                    }
+                    .opacity(binder.loaded ? 1.0 : 0.0)
+            } else {
+                Group {
+                    if let placeholder = context.placeholder, let view = placeholder(binder.progress) {
+                        view
+                    } else {
+                        Color.clear
+                    }
                 }
                 }
-                .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
+                .onAppear { [weak binder = self.binder] in
+                    guard let binder = binder else {
+                        return
+                    }
+                    if !binder.loadingOrSucceeded {
+                        binder.start(context: context)
+                    }
                 }
                 }
-                if self.cancelOnDisappear {
-                    binder.cancel()
+                .onDisappear { [weak binder = self.binder] in
+                    guard let binder = binder else {
+                        return
+                    }
+                    if context.cancelOnDisappear {
+                        binder.cancel()
+                    }
                 }
                 }
             }
             }
         }
         }