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 diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 17ff262f..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 { @@ -522,12 +521,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 } @@ -650,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` @@ -669,8 +677,8 @@ extension AnimatedImage { imageConfiguration.transition = transition return self } + #endif } -#endif #if DEBUG struct AnimatedImage_Previews : PreviewProvider { 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..78274cef 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -13,22 +13,23 @@ public struct WebImage : View { static var emptyImage = PlatformImage() var url: URL? - var placeholder: Image? var options: SDWebImageOptions var context: [SDWebImageContextOption : Any]? 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. /// - 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 +39,33 @@ 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 { + 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() + } } } - return AnyView(view) } } } @@ -135,6 +138,35 @@ extension WebImage { } } +// WebImage Modifier +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 + } + + /// 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 extension WebImage { @@ -145,9 +177,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)) } }