浏览代码

Improve KFImage lifetime based on StateObject impl

onevcat 4 年之前
父节点
当前提交
b0b935c278

+ 12 - 74
Sources/SwiftUI/ImageBinder.swift

@@ -34,56 +34,28 @@ extension KFImage {
     /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs
     /// image downloading and progress reporting based on `KingfisherManager`.
     class ImageBinder: ObservableObject {
-
-        let source: Source?
-        var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions)
+        
+        init() {
+            isLoaded = .constant(false)
+        }
 
         var downloadTask: DownloadTask?
 
         var loadingOrSucceeded: Bool {
             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 loadedImage: KFCrossPlatformImage? = nil
 
-        @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 let source = source else {
+            guard let source = context.source else {
                 CallbackQueue.mainCurrentOrAsync.execute {
-                    self.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource))
+                    context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource))
                 }
                 return
             }
@@ -91,9 +63,9 @@ extension KFImage {
             downloadTask = KingfisherManager.shared
                 .retrieveImage(
                     with: source,
-                    options: options,
+                    options: context.options,
                     progressBlock: { size, total in
-                        self.onProgressDelegate.call((size, total))
+                        context.onProgressDelegate.call((size, total))
                     },
                     completionHandler: { [weak self] result in
 
@@ -106,17 +78,17 @@ extension KFImage {
                             CallbackQueue.mainCurrentOrAsync.execute {
                                 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) }
                                 withAnimation(animation) { self.loaded = true }
                             }
 
                             CallbackQueue.mainAsync.execute {
-                                self.onSuccessDelegate.call(value)
+                                context.onSuccessDelegate.call(value)
                             }
                         case .failure(let error):
                             CallbackQueue.mainAsync.execute {
-                                self.onFailureDelegate.call(error)
+                                context.onFailureDelegate.call(error)
                             }
                         }
                 })
@@ -127,40 +99,6 @@ extension KFImage {
             downloadTask?.cancel()
             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

+ 47 - 4
Sources/SwiftUI/ImageContext.swift

@@ -30,18 +30,61 @@ import Combine
 
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFImage {
-    public struct Context<HoldingView: KFImageHoldingView> {
-        var binder: ImageBinder
+    public class Context<HoldingView: KFImageHoldingView> {
+        let source: Source?
+        var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions)
+
         var configurations: [(HoldingView) -> HoldingView] = []
+        
         var cancelOnDisappear: Bool = false
         var placeholder: AnyView? = nil
 
-        init(binder: ImageBinder) {
-            self.binder = binder
+        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
+        }
+    }
+}
+
+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)
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFAnimatedImage {

+ 0 - 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
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 struct KFImage_Previews : PreviewProvider {

+ 5 - 7
Sources/SwiftUI/KFImageOptions.swift

@@ -117,18 +117,16 @@ extension KFImageProtocol {
     /// - 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
+        context.placeholder = AnyView(v)
+        return self
     }
 
     /// Sets cancelling the download task bound to `self` when the view disappearing.
     /// - Parameter flag: Whether cancel the task or not.
     /// - Returns: A `KFImage` view that cancels downloading task when disappears.
     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.
@@ -140,7 +138,7 @@ extension KFImageProtocol {
     /// 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`.
     public func fade(duration: TimeInterval) -> Self {
-        context.binder.options.transition = .fade(duration)
+        context.options.transition = .fade(duration)
         return self
     }
 }

+ 13 - 23
Sources/SwiftUI/KFImageProtocol.swift

@@ -38,13 +38,8 @@ public protocol KFImageProtocol: View, KFOptionSetter {
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFImageProtocol {
     public var body: some View {
-        KFImageRenderer<HoldingView>(
-            binder: context.binder,
-            placeholder: context.placeholder,
-            cancelOnDisappear: context.cancelOnDisappear,
-            configurations: context.configurations
-        )
-            .id(context.binder)
+        KFImageRenderer<HoldingView>(context: context)
+            .id(context)
     }
     
     /// Starts the loading process of `self` immediately.
@@ -57,7 +52,7 @@ extension KFImageProtocol {
     /// - Returns: The `Self` value with changes applied.
     public func loadImmediately(_ start: Bool = true) -> Self {
         if start {
-            context.binder.start()
+//            context.binder.start()
         }
         return self
     }
@@ -69,8 +64,8 @@ extension KFImageProtocol {
     ///               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)
+        let context = KFImage.Context<HoldingView>(source: source)
+        self.init(context: context)
     }
 
     /// Creates a Kingfisher compatible image view to load image from the given `URL`.
@@ -82,18 +77,13 @@ extension KFImageProtocol {
     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))
-    }
     
     /// 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 `KFImage` view that configures internal `Image` with `block`.
     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 +95,15 @@ public protocol KFImageHoldingView: View {
 @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
 extension KFImageProtocol {
     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, *)
 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 {
-        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 context.placeholder != nil {
+                        context.placeholder
+                    } 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()
+                    }
                 }
             }
         }