diff --git a/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/Contents.json b/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/Contents.json new file mode 100644 index 00000000..2ee8558e --- /dev/null +++ b/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "wifi.exclamationmark.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/wifi.exclamationmark.svg b/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/wifi.exclamationmark.svg new file mode 100644 index 00000000..939aa48a --- /dev/null +++ b/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/wifi.exclamationmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index 8cae8876..a51254e1 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -22,7 +22,7 @@ class UserSettings: ObservableObject { // watchOS does not provide built-in indicator, use Espera's custom indicator extension Indicator where T == LoadingFlowerView { /// Activity Indicator - public static var activity: Indicator { + static var activity: Indicator { Indicator { isAnimating, _ in LoadingFlowerView() } @@ -31,7 +31,7 @@ extension Indicator where T == LoadingFlowerView { extension Indicator where T == StretchProgressView { /// Progress Indicator - public static var progress: Indicator { + static var progress: Indicator { Indicator { isAnimating, progress in StretchProgressView(progress: progress) } diff --git a/Example/SDWebImageSwiftUIDemo/DetailView.swift b/Example/SDWebImageSwiftUIDemo/DetailView.swift index 6a2292a9..eacbbd29 100644 --- a/Example/SDWebImageSwiftUIDemo/DetailView.swift +++ b/Example/SDWebImageSwiftUIDemo/DetailView.swift @@ -9,6 +9,31 @@ import SwiftUI import SDWebImageSwiftUI +// Placeholder when image load failed (with `.delayPlaceholder`) +#if !os(watchOS) +extension PlatformImage { + static var wifiExclamationmark: PlatformImage { + #if os(macOS) + return PlatformImage(named: "wifi.exclamationmark")! + #else + return PlatformImage(systemName: "wifi.exclamationmark")!.withTintColor(.label, renderingMode: .alwaysOriginal) + #endif + } +} +#endif + +extension Image { + static var wifiExclamationmark: Image { + #if os(macOS) + return Image("wifi.exclamationmark") + .resizable() + #else + return Image(systemName: "wifi.exclamationmark") + .resizable() + #endif + } +} + struct DetailView: View { let url: String let animated: Bool @@ -86,19 +111,22 @@ struct DetailView: View { HStack { if animated { #if os(macOS) || os(iOS) || os(tvOS) - AnimatedImage(url: URL(string:url), options: [.progressiveLoad], isAnimating: $isAnimating) - .indicator(SDWebImageProgressIndicator.default) + AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) .resizable() + .placeholder(.wifiExclamationmark) + .indicator(SDWebImageProgressIndicator.default) .scaledToFit() #else - WebImage(url: URL(string:url), options: [.progressiveLoad], isAnimating: $isAnimating) + WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) .resizable() + .placeholder(.wifiExclamationmark) .indicator(.progress) .scaledToFit() #endif } else { - WebImage(url: URL(string:url), options: [.progressiveLoad]) + WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder]) .resizable() + .placeholder(.wifiExclamationmark) .indicator(.progress) .scaledToFit() } diff --git a/README.md b/README.md index 3b207aad..54aa8bcc 100644 --- a/README.md +++ b/README.md @@ -110,20 +110,21 @@ let package = Package( ```swift var body: some View { WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic")) - .onSuccess { image, cacheType in - // Success - } - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder(Image(systemName: "photo")) // Placeholder Image - // Supports ViewBuilder as well - .placeholder { - Rectangle().foregroundColor(.gray) - } - .indicator(.activity) // Activity Indicator - .animation(.easeInOut(duration: 0.5)) // Animation Duration - .transition(.fade) // Fade Transition - .scaledToFit() - .frame(width: 300, height: 300, alignment: .center) + // Supports options and context, like `.delayPlaceholder` to show placeholder only when error + .onSuccess { image, cacheType in + // Success + } + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder(Image(systemName: "photo")) // Placeholder Image + // Supports ViewBuilder as well + .placeholder { + Rectangle().foregroundColor(.gray) + } + .indicator(.activity) // Activity Indicator + .animation(.easeInOut(duration: 0.5)) // Animation Duration + .transition(.fade) // Fade Transition + .scaledToFit() + .frame(width: 300, height: 300, alignment: .center) } ``` @@ -155,8 +156,8 @@ var body: some View { ```swift var body: some View { Group { - // Network - AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"), options: [.progressiveLoad]) // Progressive Load + AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif")) + // Supports options and context, like `.progressiveLoad` for progressive animation loading .onFailure { error in // Error } @@ -187,11 +188,13 @@ Note: `AnimatedImage` supports both image url or image data for animated image f Note: `AnimatedImage` some methods like `.transition`, `.indicator` and `.aspectRatio` have the same naming as `SwiftUI.View` protocol methods. But the args receive the different type. This is because `AnimatedImage` supports to be used with UIKit/AppKit component and animation. If you find ambiguity, use full type declaration instead of the dot expression syntax. ```swift -AnimatedImage(name: "animation2") // Just for showcase, don't mix them at the same time - .indicator(SDWebImageProgressIndicator.default) // UIKit indicator component - .indicator(Indicator.progress) // SwiftUI indicator component - .transition(SDWebImageTransition.flipFromLeft) // UIKit animation transition - .transition(AnyTransition.flipFromLeft) // SwiftUI animation transition +var body: some View { + AnimatedImage(name: "animation2") // Just for showcase, don't mix them at the same time + .indicator(SDWebImageProgressIndicator.default) // UIKit indicator component + .indicator(Indicator.progress) // SwiftUI indicator component + .transition(SDWebImageTransition.flipFromLeft) // UIKit animation transition + .transition(AnyTransition.flipFromLeft) // SwiftUI animation transition +} ``` ### Which View to choose diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 5262f84e..424552de 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -106,17 +106,7 @@ public struct WebImage : View { } } } else { - Group { - if placeholder != nil { - placeholder - } else { - // Should not use `EmptyView`, which does not respect to the container's frame modifier - // Using a empty image instead for better compatible - configurations.reduce(Image.empty) { (previous, configuration) in - configuration(previous) - } - } - } + setupPlaceholder() .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .onAppear { guard self.retryOnAppear else { return } @@ -136,6 +126,29 @@ public struct WebImage : View { } } + func configure(image: Image) -> some View { + // Should not use `EmptyView`, which does not respect to the container's frame modifier + // Using a empty image instead for better compatible + configurations.reduce(image) { (previous, configuration) in + configuration(previous) + } + } + + /// Placeholder View Support + func setupPlaceholder() -> some View { + // Don't use `Group` because it will trigger `.onAppear` and `.onDisappear` when condition view removed, treat placeholder as an entire component + if let placeholder = placeholder { + // If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :) + if imageManager.options.contains(.delayPlaceholder) && imageManager.isLoading { + return AnyView(configure(image: Image.empty)) + } else { + return placeholder + } + } else { + return AnyView(configure(image: Image.empty)) + } + } + /// Animated Image Support func setupPlayer(image: PlatformImage?) { if imagePlayer != nil {