From 124952a688d5ca1d0466758849591f880c8cafed Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 24 Jan 2021 11:26:19 +0900 Subject: [PATCH 1/9] Move image to view context --- Sources/SwiftUI/ImageBinder.swift | 16 +++++---- Sources/SwiftUI/KFImage.swift | 59 +++++++++++++++---------------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/Sources/SwiftUI/ImageBinder.swift b/Sources/SwiftUI/ImageBinder.swift index 3e2606f1c..d2ec02a6e 100644 --- a/Sources/SwiftUI/ImageBinder.swift +++ b/Sources/SwiftUI/ImageBinder.swift @@ -48,8 +48,6 @@ extension KFImage { var isLoaded: Binding - @Published var image: KFCrossPlatformImage? - @available(*, deprecated, message: "The `options` version is deprecated And will be removed soon.") init(source: Source?, options: KingfisherOptionsInfo? = nil, isLoaded: Binding) { self.source = source @@ -61,7 +59,6 @@ extension KFImage { [.loadDiskFileSynchronously] ) self.isLoaded = isLoaded - self.image = nil } init(source: Source?, isLoaded: Binding) { @@ -73,10 +70,9 @@ extension KFImage { [.loadDiskFileSynchronously] ) self.isLoaded = isLoaded - self.image = nil } - func start() { + func start(_ done: @escaping (Result) -> Void) { guard !loadingOrSucceeded else { return } @@ -103,21 +99,29 @@ extension KFImage { self.downloadTask = nil switch result { case .success(let value): + // The normalized version of 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 let image = value.image.kf.normalized + let r = RetrieveImageResult( + image: image, cacheType: value.cacheType, source: value.source, originalSource: value.originalSource + ) CallbackQueue.mainCurrentOrAsync.execute { - self.image = image + done(.success(r)) } + CallbackQueue.mainAsync.execute { self.isLoaded.wrappedValue = true self.onSuccessDelegate.call(value) } case .failure(let error): self.loadingOrSucceeded = false + CallbackQueue.mainCurrentOrAsync.execute { + done(.failure(error)) + } CallbackQueue.mainAsync.execute { self.onFailureDelegate.call(error) } diff --git a/Sources/SwiftUI/KFImage.swift b/Sources/SwiftUI/KFImage.swift index 98562b096..c9169c9c5 100644 --- a/Sources/SwiftUI/KFImage.swift +++ b/Sources/SwiftUI/KFImage.swift @@ -49,6 +49,8 @@ public struct KFImage: View { /// An image binder that manages loading and cancelling image related task. @ObservedObject private(set) var binder: ImageBinder + @State private var loadingResult: Result? + // Acts as a placeholder when loading an image. var placeholder: AnyView? @@ -73,7 +75,6 @@ public struct KFImage: View { let binder = ImageBinder(source: source, options: options, isLoaded: isLoaded) self.binder = binder configurations = [] - binder.start() } /// Creates a Kingfisher compatible image view to load image from the given `URL`. @@ -101,8 +102,6 @@ public struct KFImage: View { let binder = ImageBinder(source: source, isLoaded: isLoaded) self.binder = binder configurations = [] - // Give the `binder` a chance to accept other options. - DispatchQueue.main.async { binder.start() } } /// Creates a Kingfisher compatible image view to load image from the given `URL`. @@ -117,36 +116,34 @@ public struct KFImage: View { /// Declares the content and behavior of this view. public var body: some View { - Group { - if binder.image != nil { - configurations - .reduce(Image(crossPlatformImage: binder.image!)) { - current, config in config(current) - } - } else { - Group { - if placeholder != nil { - placeholder - } else { - Image(crossPlatformImage: .init()) - } + + if case .success(let r) = loadingResult { + configurations + .reduce(Image(crossPlatformImage: r.image)) { + current, config in config(current) + } + } else { + Group { + if placeholder != nil { + placeholder + } else { + Image(crossPlatformImage: .init()) } - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .onAppear { [weak binder = self.binder] in - guard let binder = binder else { - return - } - if !binder.loadingOrSucceeded { - binder.start() - } + } + .onAppear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if !binder.loadingOrSucceeded { + binder.start { self.loadingResult = $0 } + } + } + .onDisappear { [weak binder = self.binder] in + guard let binder = binder else { + return } - .onDisappear { [weak binder = self.binder] in - guard let binder = binder else { - return - } - if self.cancelOnDisappear { - binder.cancel() - } + if self.cancelOnDisappear { + binder.cancel() } } } From 6b9e2c1eac1cc7f4d2194da1a25458a28d8598b7 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 24 Jan 2021 14:15:43 +0900 Subject: [PATCH 2/9] Separate KFImage declaration and renderer --- Sources/General/KFOptionsSetter.swift | 12 ++-- Sources/SwiftUI/KFImage.swift | 86 +++++++++++++++++++-------- Sources/SwiftUI/KFImageOptions.swift | 4 +- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/Sources/General/KFOptionsSetter.swift b/Sources/General/KFOptionsSetter.swift index 12d52efd4..2796d0c90 100644 --- a/Sources/General/KFOptionsSetter.swift +++ b/Sources/General/KFOptionsSetter.swift @@ -45,15 +45,15 @@ extension KF.Builder: KFOptionSetter { @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension KFImage: KFOptionSetter { public var options: KingfisherParsedOptionsInfo { - get { binder.options } - nonmutating set { binder.options = newValue } + get { context.binder.options } + nonmutating set { context.binder.options = newValue } } - public var onFailureDelegate: Delegate { binder.onFailureDelegate } - public var onSuccessDelegate: Delegate { binder.onSuccessDelegate } - public var onProgressDelegate: Delegate<(Int64, Int64), Void> { binder.onProgressDelegate } + public var onFailureDelegate: Delegate { context.binder.onFailureDelegate } + public var onSuccessDelegate: Delegate { context.binder.onSuccessDelegate } + public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.binder.onProgressDelegate } - public var delegateObserver: AnyObject { binder } + public var delegateObserver: AnyObject { context.binder } } #endif diff --git a/Sources/SwiftUI/KFImage.swift b/Sources/SwiftUI/KFImage.swift index c9169c9c5..530a0cab7 100644 --- a/Sources/SwiftUI/KFImage.swift +++ b/Sources/SwiftUI/KFImage.swift @@ -40,25 +40,10 @@ extension Image { } } -/// 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, *) public struct KFImage: View { - // TODO: Replace `@ObservedObject` with `@StateObject` once we do not need to support iOS 13. - /// An image binder that manages loading and cancelling image related task. - @ObservedObject private(set) var binder: ImageBinder - - @State private var loadingResult: Result? - - // Acts as a placeholder when loading an image. - var placeholder: AnyView? - - // Whether the download task should be cancelled when the view disappears. - var cancelOnDisappear: Bool = false - - // Configurations should be performed on the image. - var configurations: [(Image) -> 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. @@ -72,9 +57,8 @@ public struct KFImage: View { /// 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 = .constant(false)) { - let binder = ImageBinder(source: source, options: options, isLoaded: isLoaded) - self.binder = binder - configurations = [] + 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`. @@ -88,10 +72,12 @@ public struct KFImage: View { /// `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.") - public init(_ url: URL?, options: KingfisherOptionsInfo? = nil, isLoaded: Binding = .constant(false)) { + init(_ url: URL?, options: KingfisherOptionsInfo? = nil, isLoaded: Binding = .constant(false)) { self.init(source: url?.convertToSource(), options: options, isLoaded: isLoaded) } + + /// 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. @@ -100,10 +86,10 @@ public struct KFImage: View { /// wrapped value from outside. public init(source: Source?, isLoaded: Binding = .constant(false)) { let binder = ImageBinder(source: source, isLoaded: isLoaded) - self.binder = binder - configurations = [] + 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. @@ -114,9 +100,59 @@ public struct KFImage: View { self.init(source: url?.convertToSource(), isLoaded: isLoaded) } - /// Declares the content and behavior of this view. + init(binder: ImageBinder) { + self.context = Context(binder: binder) + } + public var body: some View { + KFImageRenderer(context) + .id(context.binder.source?.url?.absoluteString ?? "-1") + } +} + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +extension KFImage { + struct Context { + var binder: ImageBinder + var configurations: [(Image) -> Image] = [] + var cancelOnDisappear: Bool = false + var placeholder: AnyView? = nil + + init(binder: ImageBinder) { + self.binder = binder + } + } +} + +/// 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 { + + // TODO: Replace `@ObservedObject` with `@StateObject` once we do not need to support iOS 13. + /// An image binder that manages loading and cancelling image related task. + @ObservedObject private(set) var binder: KFImage.ImageBinder + @State private var loadingResult: Result? + + // 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. + var body: some View { if case .success(let r) = loadingResult { configurations .reduce(Image(crossPlatformImage: r.image)) { @@ -159,7 +195,7 @@ extension KFImage { /// - Returns: A `KFImage` view that configures internal `Image` with `block`. public func configure(_ block: @escaping (Image) -> Image) -> KFImage { var result = self - result.configurations.append(block) + result.context.configurations.append(block) return result } @@ -188,7 +224,7 @@ extension KFImage { struct KFImage_Previews : PreviewProvider { static var previews: some View { Group { - KFImage(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!) + KFImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!)) .onSuccess { r in print(r) } diff --git a/Sources/SwiftUI/KFImageOptions.swift b/Sources/SwiftUI/KFImageOptions.swift index 16f2e3e1f..077a809c1 100644 --- a/Sources/SwiftUI/KFImageOptions.swift +++ b/Sources/SwiftUI/KFImageOptions.swift @@ -113,7 +113,7 @@ extension KFImage { public func placeholder(@ViewBuilder _ content: () -> Content) -> KFImage { let v = content() var result = self - result.placeholder = AnyView(v) + result.context.placeholder = AnyView(v) return result } @@ -122,7 +122,7 @@ extension KFImage { /// - Returns: A `KFImage` view that cancels downloading task when disappears. public func cancelOnDisappear(_ flag: Bool) -> KFImage { var result = self - result.cancelOnDisappear = flag + result.context.cancelOnDisappear = flag return result } } From 691f1053b40b5f6f28d034932d4ebdb705c990bf Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 24 Jan 2021 15:19:58 +0900 Subject: [PATCH 3/9] Use source as hash id --- Sources/General/ImageSource/Source.swift | 26 ++++++++++++++++++++++++ Sources/SwiftUI/ImageBinder.swift | 2 +- Sources/SwiftUI/KFImage.swift | 10 +++------ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Sources/General/ImageSource/Source.swift b/Sources/General/ImageSource/Source.swift index 79680abf4..0fcf28b25 100644 --- a/Sources/General/ImageSource/Source.swift +++ b/Sources/General/ImageSource/Source.swift @@ -80,6 +80,32 @@ public enum Source { } } +extension Source: Hashable { + public static func == (lhs: Source, rhs: Source) -> Bool { + switch (lhs, rhs) { + case (.network(let r1), .network(let r2)): + return r1.cacheKey == r2.cacheKey && r1.downloadURL == r2.downloadURL + case (.provider(let p1), .provider(let p2)): + return p1.cacheKey == p2.cacheKey && p1.contentURL == p2.contentURL + case (.provider(_), .network(_)): + return false + case (.network(_), .provider(_)): + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .network(let r): + hasher.combine(r.cacheKey) + hasher.combine(r.downloadURL) + case .provider(let p): + hasher.combine(p.cacheKey) + hasher.combine(p.contentURL) + } + } +} + extension Source { var asResource: Resource? { guard case .network(let resource) = self else { diff --git a/Sources/SwiftUI/ImageBinder.swift b/Sources/SwiftUI/ImageBinder.swift index d2ec02a6e..885145d5d 100644 --- a/Sources/SwiftUI/ImageBinder.swift +++ b/Sources/SwiftUI/ImageBinder.swift @@ -33,7 +33,7 @@ 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 { + class ImageBinder { let source: Source? var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions) diff --git a/Sources/SwiftUI/KFImage.swift b/Sources/SwiftUI/KFImage.swift index 530a0cab7..877c18b19 100644 --- a/Sources/SwiftUI/KFImage.swift +++ b/Sources/SwiftUI/KFImage.swift @@ -76,8 +76,6 @@ public struct KFImage: View { self.init(source: url?.convertToSource(), options: options, isLoaded: isLoaded) } - - /// 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. @@ -89,7 +87,6 @@ public struct KFImage: View { 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. @@ -106,7 +103,7 @@ public struct KFImage: View { public var body: some View { KFImageRenderer(context) - .id(context.binder.source?.url?.absoluteString ?? "-1") + .id(context.binder.source) } } @@ -129,9 +126,8 @@ extension KFImage { @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) struct KFImageRenderer: View { - // TODO: Replace `@ObservedObject` with `@StateObject` once we do not need to support iOS 13. /// An image binder that manages loading and cancelling image related task. - @ObservedObject private(set) var binder: KFImage.ImageBinder + private let binder: KFImage.ImageBinder @State private var loadingResult: Result? @@ -163,7 +159,7 @@ struct KFImageRenderer: View { if placeholder != nil { placeholder } else { - Image(crossPlatformImage: .init()) + Color.clear } } .onAppear { [weak binder = self.binder] in From dda7afedfd8f90b63cc8208c93383e9ef3dacbf4 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 24 Jan 2021 15:35:53 +0900 Subject: [PATCH 4/9] Use binder as hash id --- .../Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift | 7 +++++++ Sources/SwiftUI/ImageBinder.swift | 12 ++++++++++++ Sources/SwiftUI/KFImage.swift | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift b/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift index 89ec43a1c..6ed8358f3 100644 --- a/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift +++ b/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift @@ -32,6 +32,8 @@ struct SwiftUIView : View { @State private var index = 1 + @State private var blackWhite = false + var url: URL { URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher-\(self.index).jpg")! } @@ -39,6 +41,8 @@ struct SwiftUIView : View { var body: some View { VStack { KFImage(url) + .cacheOriginalImage() + .setProcessor(blackWhite ? BlackWhiteProcessor() : DefaultImageProcessor()) .onSuccess { r in print("suc: \(r)") } @@ -57,6 +61,9 @@ struct SwiftUIView : View { Button(action: { self.index = (self.index % 10) + 1 }) { Text("Next Image") } + Button(action: { + self.blackWhite.toggle() + }) { Text("Black & White") } }.navigationBarTitle(Text("Basic Image"), displayMode: .inline) } diff --git a/Sources/SwiftUI/ImageBinder.swift b/Sources/SwiftUI/ImageBinder.swift index 885145d5d..76e7ec14a 100644 --- a/Sources/SwiftUI/ImageBinder.swift +++ b/Sources/SwiftUI/ImageBinder.swift @@ -135,4 +135,16 @@ extension KFImage { } } } + +@available(iOSApplicationExtension 13.0, *) +extension KFImage.ImageBinder: Hashable { + static func == (lhs: KFImage.ImageBinder, rhs: KFImage.ImageBinder) -> Bool { + return lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(source) + hasher.combine(options.processor.identifier) + } +} #endif diff --git a/Sources/SwiftUI/KFImage.swift b/Sources/SwiftUI/KFImage.swift index 877c18b19..624405934 100644 --- a/Sources/SwiftUI/KFImage.swift +++ b/Sources/SwiftUI/KFImage.swift @@ -103,7 +103,7 @@ public struct KFImage: View { public var body: some View { KFImageRenderer(context) - .id(context.binder.source) + .id(context.binder) } } From b6c12cfabd053bba5e903414bf4cd1ac9d3fb000 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 24 Jan 2021 17:35:56 +0900 Subject: [PATCH 5/9] Update available --- Sources/SwiftUI/ImageBinder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUI/ImageBinder.swift b/Sources/SwiftUI/ImageBinder.swift index 76e7ec14a..a1c9522f7 100644 --- a/Sources/SwiftUI/ImageBinder.swift +++ b/Sources/SwiftUI/ImageBinder.swift @@ -136,7 +136,7 @@ extension KFImage { } } -@available(iOSApplicationExtension 13.0, *) +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension KFImage.ImageBinder: Hashable { static func == (lhs: KFImage.ImageBinder, rhs: KFImage.ImageBinder) -> Bool { return lhs === rhs From d697cf3efcff8c0f18f6138675ff72e6740b5e3c Mon Sep 17 00:00:00 2001 From: onevcat Date: Wed, 27 Jan 2021 23:19:53 +0900 Subject: [PATCH 6/9] Make fade work as in KFImage --- .../Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift | 2 -- Sources/SwiftUI/KFImage.swift | 10 +++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift b/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift index 0dc65e3b5..3d741638f 100644 --- a/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift +++ b/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift @@ -79,8 +79,6 @@ struct SwiftUIList : View { .aspectRatio(contentMode: .fit) .cornerRadius(20) .frame(width: 300, height: 300) - .opacity(done || alreadyCached ? 1.0 : 0.3) - .animation(.linear(duration: 0.4)) Spacer() }.padding(.vertical, 12) diff --git a/Sources/SwiftUI/KFImage.swift b/Sources/SwiftUI/KFImage.swift index 624405934..a97acde41 100644 --- a/Sources/SwiftUI/KFImage.swift +++ b/Sources/SwiftUI/KFImage.swift @@ -130,6 +130,7 @@ struct KFImageRenderer: View { private let binder: KFImage.ImageBinder @State private var loadingResult: Result? + @State private var isLoaded = false // Acts as a placeholder when loading an image. var placeholder: AnyView? @@ -154,6 +155,8 @@ struct KFImageRenderer: View { .reduce(Image(crossPlatformImage: r.image)) { current, config in config(current) } + .opacity(isLoaded ? 1.0 : 0.0) + .animation(.default) } else { Group { if placeholder != nil { @@ -167,7 +170,12 @@ struct KFImageRenderer: View { return } if !binder.loadingOrSucceeded { - binder.start { self.loadingResult = $0 } + binder.start { + self.loadingResult = $0 + loadingResult?.matchSuccess { _ in + CallbackQueue.mainAsync.execute { isLoaded = true } + } + } } } .onDisappear { [weak binder = self.binder] in From 1f675477787415d519f1a47af627e6719d60c633 Mon Sep 17 00:00:00 2001 From: onevcat Date: Thu, 28 Jan 2021 00:22:22 +0900 Subject: [PATCH 7/9] Support fade and force transition for KFImage --- .../SwiftUIViews/SwiftUIList.swift | 1 + .../SwiftUIViews/SwiftUIView.swift | 2 ++ Sources/Extensions/ImageView+Kingfisher.swift | 5 ++- Sources/General/KF.swift | 8 ----- Sources/General/KFOptionsSetter.swift | 9 +++++ Sources/Image/ImageTransition.swift | 3 ++ Sources/SwiftUI/KFImage.swift | 36 +++++++++++++++++-- Sources/SwiftUI/KFImageOptions.swift | 13 +++++++ 8 files changed, 65 insertions(+), 12 deletions(-) diff --git a/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift b/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift index 3d741638f..05fcd85cc 100644 --- a/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift +++ b/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIList.swift @@ -75,6 +75,7 @@ struct SwiftUIList : View { } .foregroundColor(.gray) } + .fade(duration: 1) .cancelOnDisappear(true) .aspectRatio(contentMode: .fit) .cornerRadius(20) diff --git a/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift b/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift index 6ed8358f3..6f56ea39e 100644 --- a/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift +++ b/Demo/Demo/Kingfisher-Demo/SwiftUIViews/SwiftUIView.swift @@ -53,6 +53,8 @@ struct SwiftUIView : View { Image(systemName: "arrow.2.circlepath.circle") .font(.largeTitle) } + .fade(duration: 1) + .forceTransition() .resizable() .frame(width: 300, height: 300) .cornerRadius(20) diff --git a/Sources/Extensions/ImageView+Kingfisher.swift b/Sources/Extensions/ImageView+Kingfisher.swift index 9e3d99fcf..79c33c0b9 100644 --- a/Sources/Extensions/ImageView+Kingfisher.swift +++ b/Sources/Extensions/ImageView+Kingfisher.swift @@ -383,7 +383,10 @@ extension KingfisherWrapper where Base: KFCrossPlatformImageView { switch options.transition { case .none: return false - #if !os(macOS) + #if os(macOS) + case .fade: // Fade is only a placeholder for SwiftUI on macOS. + return false + #else default: if options.forceTransition { return true } if cacheType == .none { return true } diff --git a/Sources/General/KF.swift b/Sources/General/KF.swift index b764c93cd..ca2411a01 100644 --- a/Sources/General/KF.swift +++ b/Sources/General/KF.swift @@ -333,14 +333,6 @@ extension KF.Builder { } #endif - /// Sets whether the image setting for an image view should happen with transition even when retrieved from cache. - /// - Parameter enabled: Enable the force transition or not. - /// - Returns: A `KF.Builder` with changes applied. - public func forceTransition(_ enabled: Bool = true) -> Self { - options.forceTransition = enabled - return self - } - /// Sets whether keeping the existing image of image view while setting another image to it. /// - Parameter enabled: Whether the existing image should be kept. /// - Returns: A `KF.Builder` with changes applied. diff --git a/Sources/General/KFOptionsSetter.swift b/Sources/General/KFOptionsSetter.swift index 2796d0c90..226146d3f 100644 --- a/Sources/General/KFOptionsSetter.swift +++ b/Sources/General/KFOptionsSetter.swift @@ -339,6 +339,15 @@ extension KFOptionSetter { options.lowDataModeSource = source return self } + + /// Sets whether the image setting for an image view should happen with transition even when retrieved from cache. + /// - Parameter enabled: Enable the force transition or not. + /// - Returns: A `KF.Builder` with changes applied. + public func forceTransition(_ enabled: Bool = true) -> Self { + options.forceTransition = enabled + return self + } + } // MARK: - Request Modifier diff --git a/Sources/Image/ImageTransition.swift b/Sources/Image/ImageTransition.swift index c13a9d227..4d042dffb 100644 --- a/Sources/Image/ImageTransition.swift +++ b/Sources/Image/ImageTransition.swift @@ -24,6 +24,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import Foundation #if os(iOS) || os(tvOS) import UIKit @@ -111,5 +112,7 @@ public enum ImageTransition { // Just a placeholder for compiling on macOS. public enum ImageTransition { case none + /// This is a placeholder on macOS now. It is for SwiftUI (KFImage) to identify the fade option only. + case fade(TimeInterval) } #endif diff --git a/Sources/SwiftUI/KFImage.swift b/Sources/SwiftUI/KFImage.swift index a97acde41..2f0c2a04f 100644 --- a/Sources/SwiftUI/KFImage.swift +++ b/Sources/SwiftUI/KFImage.swift @@ -156,7 +156,6 @@ struct KFImageRenderer: View { current, config in config(current) } .opacity(isLoaded ? 1.0 : 0.0) - .animation(.default) } else { Group { if placeholder != nil { @@ -172,8 +171,19 @@ struct KFImageRenderer: View { if !binder.loadingOrSucceeded { binder.start { self.loadingResult = $0 - loadingResult?.matchSuccess { _ in - CallbackQueue.mainAsync.execute { isLoaded = true } + switch $0 { + case .success(let result): + CallbackQueue.mainAsync.execute { + if let duration = fadeTransitionDuration(cacheType: result.cacheType) { + withAnimation(.linear(duration: duration)) { + isLoaded = true + } + } else { + isLoaded = true + } + } + case .failure(_): + break } } } @@ -188,6 +198,26 @@ struct KFImageRenderer: View { } } } + + private func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? { + if binder.options.forceTransition || cacheType == .none { + return binder.options.transition.fadeDuration + } else { + return nil + } + } +} + +extension ImageTransition { + // Only for fade effect in SwiftUI. + var fadeDuration: TimeInterval? { + switch self { + case .fade(let duration): + return duration + default: + return nil + } + } } // MARK: - Image compatibility. diff --git a/Sources/SwiftUI/KFImageOptions.swift b/Sources/SwiftUI/KFImageOptions.swift index 077a809c1..64b242cf5 100644 --- a/Sources/SwiftUI/KFImageOptions.swift +++ b/Sources/SwiftUI/KFImageOptions.swift @@ -125,5 +125,18 @@ extension KFImage { result.context.cancelOnDisappear = flag return result } + + /// Sets a fade transition for the image task. + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A `KFImage` 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 `KFImage`. + public func fade(duration: TimeInterval) -> KFImage { + context.binder.options.transition = .fade(duration) + return self + } } #endif From 1b2eb05f0df52596173ed2f2b64ea8d595ebfb6e Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 30 Jan 2021 22:39:11 +0900 Subject: [PATCH 8/9] Minor refactor --- Sources/SwiftUI/KFImage.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftUI/KFImage.swift b/Sources/SwiftUI/KFImage.swift index 2f0c2a04f..f470d3f0c 100644 --- a/Sources/SwiftUI/KFImage.swift +++ b/Sources/SwiftUI/KFImage.swift @@ -199,18 +199,20 @@ struct KFImageRenderer: View { } } + private func shouldApplyFade(cacheType: CacheType) -> Bool { + binder.options.forceTransition || cacheType == .none + } + private func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? { - if binder.options.forceTransition || cacheType == .none { - return binder.options.transition.fadeDuration - } else { - return nil - } + shouldApplyFade(cacheType: cacheType) + ? binder.options.transition.fadeDuration + : nil } } extension ImageTransition { // Only for fade effect in SwiftUI. - var fadeDuration: TimeInterval? { + fileprivate var fadeDuration: TimeInterval? { switch self { case .fade(let duration): return duration From 9fa6a835f22c6ea38758573357fff98b6b4d403a Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 30 Jan 2021 22:46:36 +0900 Subject: [PATCH 9/9] Better code readability --- Sources/SwiftUI/KFImage.swift | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftUI/KFImage.swift b/Sources/SwiftUI/KFImage.swift index f470d3f0c..033cef0bc 100644 --- a/Sources/SwiftUI/KFImage.swift +++ b/Sources/SwiftUI/KFImage.swift @@ -169,18 +169,14 @@ struct KFImageRenderer: View { return } if !binder.loadingOrSucceeded { - binder.start { - self.loadingResult = $0 - switch $0 { - case .success(let result): + binder.start { result in + self.loadingResult = result + switch result { + case .success(let value): CallbackQueue.mainAsync.execute { - if let duration = fadeTransitionDuration(cacheType: result.cacheType) { - withAnimation(.linear(duration: duration)) { - isLoaded = true - } - } else { - isLoaded = true - } + let animation = fadeTransitionDuration(cacheType: value.cacheType) + .map { duration in Animation.linear(duration: duration) } + withAnimation(animation) { isLoaded = true } } case .failure(_): break