From 9cdd592c0392d34fb411de10506f43d434880259 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 2 Nov 2019 12:33:27 +0800 Subject: [PATCH 1/5] Refactory placeholder arg with a view modifier API, make it more suitable for common usage --- .../Classes/Indicator/Indicator.swift | 22 +++--- SDWebImageSwiftUI/Classes/WebImage.swift | 69 +++++++++++-------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift index e146e0bd..16f942ba 100644 --- a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift +++ b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift @@ -11,15 +11,15 @@ import SwiftUI /// A type to build the indicator public struct Indicator where T : View { - var builder: (Binding, Binding) -> T + var content: (Binding, Binding) -> T /// Create a indicator with builder /// - Parameter builder: A builder to build indicator /// - Parameter isAnimating: A Binding to control the animation. If image is during loading, the value is true, else (like start loading) the value is false. /// - Parameter progress: A Binding to control the progress during loading. If no progress can be reported, the value is 0. /// Associate a indicator when loading image with url - public init(@ViewBuilder builder: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) { - self.builder = builder + public init(@ViewBuilder content: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) { + self.content = content } } @@ -32,17 +32,17 @@ struct IndicatorViewModifier : ViewModifier where T : View { var indicator: Indicator func body(content: Content) -> some View { - if imageManager.isFinished { - // Disable Indiactor - return AnyView(content) - } else { - // Enable indicator - return AnyView( + Group { + if imageManager.isFinished { + // Disable Indiactor + content + } else { + // Enable indicator ZStack { content - indicator.builder($imageManager.isLoading, $imageManager.progress) + indicator.content($imageManager.isLoading, $imageManager.progress) } - ) + } } } } diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index b59fb6c8..f2be5966 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -13,22 +13,20 @@ public struct WebImage : View { static var emptyImage = PlatformImage() var url: URL? - var placeholder: Image? var options: SDWebImageOptions var context: [SDWebImageContextOption : Any]? + var placeholder: AnyView? var configurations: [(Image) -> Image] = [] @ObservedObject var imageManager: ImageManager /// Create a web image with url, placeholder, custom options and context. /// - Parameter url: The image url - /// - Parameter placeholder: The placeholder image to show during loading /// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values. /// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. - public init(url: URL?, placeholder: Image? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) { + public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) { self.url = url - self.placeholder = placeholder self.options = options self.context = context self.imageManager = ImageManager(url: url, options: options, context: context) @@ -38,31 +36,31 @@ public struct WebImage : View { } public var body: some View { - if let platformImage = imageManager.image { - var image = Image(platformImage: platformImage) - image = configurations.reduce(image) { (previous, configuration) in - configuration(previous) - } - let view = image - return AnyView(view) - } else { - var image = placeholder ?? Image(platformImage: WebImage.emptyImage) - image = configurations.reduce(image) { (previous, configuration) in - configuration(previous) - } - let view = image - .onAppear { - if !self.imageManager.isFinished { - self.imageManager.load() + Group { + if imageManager.image != nil { + configurations.reduce(Image(platformImage: imageManager.image!)) { (previous, configuration) in + configuration(previous) } - } - .onDisappear { - // When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case - if self.imageManager.isLoading && !self.imageManager.isIncremental { - self.imageManager.cancel() + } else { + Group { + if placeholder != nil { + placeholder + } else { + Image(platformImage: WebImage.emptyImage) + } + } + .onAppear { + if !self.imageManager.isFinished { + self.imageManager.load() + } + } + .onDisappear { + // When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case + if self.imageManager.isLoading && !self.imageManager.isIncremental { + self.imageManager.cancel() + } } } - return AnyView(view) } } } @@ -135,6 +133,19 @@ extension WebImage { } } +// Placeholder +extension WebImage { + + /// Associate a placeholder when loading image with url + /// - note: The differences between Placeholder and Indicator, is that placeholder does not supports animation, and return type is different + /// - Parameter content: A view that describes the placeholder. + public func placeholder(@ViewBuilder _ content: () -> T) -> WebImage where T : View { + var result = self + result.placeholder = AnyView(content()) + return result + } +} + // Indicator extension WebImage { @@ -145,9 +156,9 @@ extension WebImage { } /// Associate a indicator when loading image with url, convenient method with block - /// - Parameter indicator: The indicator type, see `Indicator` - public func indicator(@ViewBuilder builder: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) -> some View where T : View { - return indicator(Indicator(builder: builder)) + /// - Parameter content: A view that describes the indicator. + public func indicator(@ViewBuilder content: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) -> some View where T : View { + return indicator(Indicator(content: content)) } } From 8f2054ee20405ebe63add4cf9d230fbfa1a05826 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 2 Nov 2019 12:40:47 +0800 Subject: [PATCH 2/5] Change the AnimatedImage AnyView into Group, seems this is more performent --- SDWebImageSwiftUI/Classes/AnimatedImage.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 17ff262f..78e169f7 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -522,12 +522,14 @@ extension AnimatedImage { #if os(macOS) || os(iOS) || os(tvOS) return self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode) #else - if let aspectRatio = aspectRatio { - return AnyView(self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)) - } else { - // on watchOS, there are no workaround like `AnimatedImageViewWrapper` to override `intrinsicContentSize`, so the aspect ratio is undetermined and cause sizing issues - // To workaround, we do not call default implementation for this case, using original solution instead - return AnyView(self) + return Group { + if aspectRatio != nil { + self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode) + } else { + // on watchOS, there are no workaround like `AnimatedImageViewWrapper` to override `intrinsicContentSize`, so the aspect ratio is undetermined and cause sizing issues + // To workaround, we do not call default implementation for this case, using original solution instead + self + } } #endif } From df96e832b8928b4678cd0d6dd8cb446fafda413c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 2 Nov 2019 12:47:28 +0800 Subject: [PATCH 3/5] Add `retryOnAppear` and `cancelOnDisappear` behavior control --- SDWebImageSwiftUI/Classes/WebImage.swift | 25 ++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index f2be5966..c1eb1616 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -16,9 +16,12 @@ public struct WebImage : View { var options: SDWebImageOptions var context: [SDWebImageContextOption : Any]? - var placeholder: AnyView? var configurations: [(Image) -> Image] = [] + var placeholder: AnyView? + var retryOnAppear: Bool = true + var cancelOnDisappear: Bool = true + @ObservedObject var imageManager: ImageManager /// Create a web image with url, placeholder, custom options and context. @@ -50,11 +53,13 @@ public struct WebImage : View { } } .onAppear { + guard self.retryOnAppear else { return } if !self.imageManager.isFinished { self.imageManager.load() } } .onDisappear { + guard self.cancelOnDisappear else { return } // When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case if self.imageManager.isLoading && !self.imageManager.isIncremental { self.imageManager.cancel() @@ -133,7 +138,7 @@ extension WebImage { } } -// Placeholder +// Custom Configuration extension WebImage { /// Associate a placeholder when loading image with url @@ -144,6 +149,22 @@ extension WebImage { result.placeholder = AnyView(content()) return result } + + /// Control the behavior to retry the failed loading when view become appears again + /// - Parameter flag: Whether or not to retry the failed loading + public func retryOnAppear(_ flag: Bool) -> WebImage { + var result = self + result.retryOnAppear = flag + return result + } + + /// Control the behavior to cancel the pending loading when view become disappear again + /// - Parameter flag: Whether or not to cancel the pending loading + public func cancelOnDisappear(_ flag: Bool) -> WebImage { + var result = self + result.cancelOnDisappear = flag + return result + } } // Indicator From fc9c2b52936e18d3b564f6ff9e8f04a02ec37500 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 2 Nov 2019 13:12:43 +0800 Subject: [PATCH 4/5] Change the AnimatedImage API to match the same design. But AnimatedImage need the platform image (becaseu this is needed for SD and return type is limited to AnimatedImage) --- SDWebImageSwiftUI/Classes/AnimatedImage.swift | 22 ++++++++++++------- SDWebImageSwiftUI/Classes/WebImage.swift | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 78e169f7..5115b624 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -47,6 +47,7 @@ final class AnimatedImageConfiguration: ObservableObject { @Published var indicator: SDWebImageIndicator? @Published var transition: SDWebImageTransition? #endif + @Published var placeholder: PlatformImage? } // Convenient @@ -67,7 +68,6 @@ public struct AnimatedImage : PlatformViewRepresentable { @ObservedObject var imageCoordinator = AnimatedImageCoordinator() var url: URL? - var placeholder: PlatformImage? var webOptions: SDWebImageOptions = [] var webContext: [SDWebImageContextOption : Any]? = nil @@ -80,8 +80,8 @@ public struct AnimatedImage : PlatformViewRepresentable { /// - Parameter placeholder: The placeholder image to show during loading /// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values. /// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. - public init(url: URL?, placeholder: PlatformImage? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) { - self.init(url: url, placeholder: placeholder, options: options, context: context, isAnimating: .constant(true)) + public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) { + self.init(url: url, options: options, context: context, isAnimating: .constant(true)) } /// Create an animated image with url, placeholder, custom options and context, including animation control binding. @@ -90,9 +90,8 @@ public struct AnimatedImage : PlatformViewRepresentable { /// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values. /// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. /// - Parameter isAnimating: The binding for animation control - public init(url: URL?, placeholder: PlatformImage? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding) { + public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding) { self._isAnimating = isAnimating - self.placeholder = placeholder self.webOptions = options self.webContext = context self.url = url @@ -190,7 +189,7 @@ public struct AnimatedImage : PlatformViewRepresentable { if currentOperation != nil { return } - view.wrapped.sd_setImage(with: url, placeholderImage: placeholder, options: webOptions, context: webContext, progress: { (receivedSize, expectedSize, _) in + view.wrapped.sd_setImage(with: url, placeholderImage: imageConfiguration.placeholder, options: webOptions, context: webContext, progress: { (receivedSize, expectedSize, _) in self.imageModel.progressBlock?(receivedSize, expectedSize) }) { (image, error, cacheType, _) in if let image = image { @@ -652,10 +651,17 @@ extension AnimatedImage { } } -#if os(macOS) || os(iOS) || os(tvOS) // Web Image convenience extension AnimatedImage { + /// Associate a placeholder when loading image with url + /// - Parameter content: A view that describes the placeholder. + public func placeholder(_ placeholder: PlatformImage?) -> AnimatedImage { + imageConfiguration.placeholder = placeholder + return self + } + + #if os(macOS) || os(iOS) || os(tvOS) /// Associate a indicator when loading image with url /// - Note: If you do not need indicator, specify nil. Defaults to nil /// - Parameter indicator: indicator, see more in `SDWebImageIndicator` @@ -671,8 +677,8 @@ extension AnimatedImage { imageConfiguration.transition = transition return self } + #endif } -#endif #if DEBUG struct AnimatedImage_Previews : PreviewProvider { diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index c1eb1616..78274cef 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -138,7 +138,7 @@ extension WebImage { } } -// Custom Configuration +// WebImage Modifier extension WebImage { /// Associate a placeholder when loading image with url From 9dcc697a16fa0c957a9ae9cd4da2ac131eab5135 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 2 Nov 2019 14:12:20 +0800 Subject: [PATCH 5/5] Update the demo for placeholder comments --- Example/SDWebImageSwiftUIDemo/ContentView.swift | 8 ++++++++ README.md | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index fe46639a..cc3b339a 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -86,6 +86,9 @@ struct ContentView: View { #if os(macOS) || os(iOS) || os(tvOS) AnimatedImage(url: URL(string:url)) .indicator(SDWebImageActivityIndicator.medium) + /** + .placeholder(UIImage(systemName: "photo")) + */ .transition(.fade) .resizable() .scaledToFit() @@ -100,6 +103,11 @@ struct ContentView: View { #if os(macOS) || os(iOS) || os(tvOS) WebImage(url: URL(string:url)) .resizable() + /** + .placeholder { + Image(systemName: "photo") + } + */ .indicator(.activity) .animation(.easeInOut(duration: 0.5)) .transition(.fade) diff --git a/README.md b/README.md index 0737ac48..06ecec40 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,11 @@ var body: some View { // Success } .resizable() // Resizable like SwiftUI.Image + .placeholder { + Image(systemName: "photo") // Placeholder + } .indicator(.activity) // Activity Indicator - .animation(.easeInOut(duration: 0.5)) + .animation(.easeInOut(duration: 0.5)) // Animation Duration .transition(.fade) // Fade Transition .scaledToFit() .frame(width: 300, height: 300, alignment: .center) @@ -118,6 +121,7 @@ var body: some View { // Error } .resizable() // Actually this is not needed unlike SwiftUI.Image + .placeholder(UIImage(systemName: "photo")) // Placeholder .indicator(SDWebImageActivityIndicator.medium) // Activity Indicator .transition(.fade) // Fade Transition .scaledToFit() // Attention to call it on AnimatedImage, but not `some View` after View Modifier