From 4c4b868b79b2fdd075ba62eb914228afe8e35661 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 18 Mar 2024 17:21:42 +0800 Subject: [PATCH 1/2] Fix the issue for WebImage when url is nil will not cause the reloading --- .../SDWebImageSwiftUIDemo/ContentView.swift | 39 ++++++++++++++++++- SDWebImageSwiftUI/Classes/WebImage.swift | 9 +++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index e27bdb7..743af5b 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -17,6 +17,43 @@ class UserSettings: ObservableObject { #endif } +// Test Switching nil url +struct ContentView: View { + @State var isOn = false + @State var animated: Bool = false // You can change between WebImage/AnimatedImage + + var url: URL? { + if isOn { + .init(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/1024px-Google_%22G%22_logo.svg.png") + } else { + nil + } + } + + var body: some View { + VStack { + Text("\(animated ? "AnimatedImage" : "WebImage")") + Spacer() + if animated { + AnimatedImage(url: url) + .resizable() + .scaledToFit() + .frame(width: 100, height: 100) + } else { + WebImage(url: url) + .resizable() + .scaledToFit() + .frame(width: 100, height: 100) + } + Button("Toggle \(isOn ? "nil" : "valid") URL") { + isOn.toggle() + } + Spacer() + Toggle("Switch", isOn: $animated) + } + } +} + // Test Switching url using @State struct ContentView2: View { @State var imageURLs = [ @@ -63,7 +100,7 @@ struct ContentView2: View { } } -struct ContentView: View { +struct ContentView3: View { @State var imageURLs = [ "http://assets.sbnation.com/assets/2512203/dogflops.gif", "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif", diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 457b6e6..4b0d091 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -163,6 +163,7 @@ public struct WebImage : View where Content: View { } } else { content((imageManager.error != nil) ? .failure(imageManager.error!) : .empty) + setupPlaceholder() // Load Logic .onPlatformAppear(appear: { self.setupManager() @@ -326,6 +327,14 @@ public struct WebImage : View where Content: View { } } } + + /// Placeholder View Support + func setupPlaceholder() -> some View { + let result = content((imageManager.error != nil) ? .failure(imageManager.error!) : .empty) + // Custom ID to avoid SwiftUI engine cache the status, and does not call `onAppear` when placeholder not changed (See `ContentView.swift/ContentView2` case) + // Because we load the image url in placeholder's `onAppear`, it should be called to sync with state changes :) + return result.id(imageModel.url) + } } // Layout From 6ba07e3c18010f591c7ce045b795a84fa1bc065b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 18 Mar 2024 17:36:51 +0800 Subject: [PATCH 2/2] Fix the issue for AnimatedImage when url is nil will not cause the reloading --- .../SDWebImageSwiftUIDemo/ContentView.swift | 4 +- SDWebImageSwiftUI/Classes/AnimatedImage.swift | 122 +++++++++++------- 2 files changed, 76 insertions(+), 50 deletions(-) diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index 743af5b..e31d281 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -18,7 +18,7 @@ class UserSettings: ObservableObject { } // Test Switching nil url -struct ContentView: View { +struct ContentView3: View { @State var isOn = false @State var animated: Bool = false // You can change between WebImage/AnimatedImage @@ -100,7 +100,7 @@ struct ContentView2: View { } } -struct ContentView3: View { +struct ContentView: View { @State var imageURLs = [ "http://assets.sbnation.com/assets/2512203/dogflops.gif", "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif", diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 2357a8f..fd2fdf5 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -27,6 +27,13 @@ public final class AnimatedImageCoordinator: NSObject { /// Data Binding Object, only properties in this object can support changes from user with @State and refresh @available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) final class AnimatedImageModel : ObservableObject { + enum Kind { + case url + case data + case name + case unknown + } + var kind: Kind = .unknown /// URL image @Published var url: URL? @Published var webOptions: SDWebImageOptions = [] @@ -123,6 +130,7 @@ public struct AnimatedImage : PlatformViewRepresentable { /// - Parameter isAnimating: The binding for animation control public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding = .constant(true), placeholderImage: PlatformImage? = nil) { let imageModel = AnimatedImageModel() + imageModel.kind = .url imageModel.url = url imageModel.webOptions = options imageModel.webContext = context @@ -138,6 +146,7 @@ public struct AnimatedImage : PlatformViewRepresentable { /// - Parameter isAnimating: The binding for animation control public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding = .constant(true), @ViewBuilder placeholder: @escaping () -> T) where T : View { let imageModel = AnimatedImageModel() + imageModel.kind = .url imageModel.url = url imageModel.webOptions = options imageModel.webContext = context @@ -157,6 +166,7 @@ public struct AnimatedImage : PlatformViewRepresentable { /// - Parameter isAnimating: The binding for animation control public init(name: String, bundle: Bundle? = nil, isAnimating: Binding = .constant(true)) { let imageModel = AnimatedImageModel() + imageModel.kind = .name imageModel.name = name imageModel.bundle = bundle self.init(imageModel: imageModel, isAnimating: isAnimating) @@ -168,6 +178,7 @@ public struct AnimatedImage : PlatformViewRepresentable { /// - Parameter isAnimating: The binding for animation control public init(data: Data, scale: CGFloat = 1, isAnimating: Binding = .constant(true)) { let imageModel = AnimatedImageModel() + imageModel.kind = .data imageModel.data = data imageModel.scale = scale self.init(imageModel: imageModel, isAnimating: isAnimating) @@ -275,57 +286,72 @@ public struct AnimatedImage : PlatformViewRepresentable { return view } + private func updateViewForName(_ name: String, view: AnimatedImageViewWrapper, context: Context) { + var image: PlatformImage? + #if os(macOS) + image = SDAnimatedImage(named: name, in: imageModel.bundle) + if image == nil { + // For static image, use NSImage as defaults + let bundle = imageModel.bundle ?? .main + image = bundle.image(forResource: name) + } + #else + image = SDAnimatedImage(named: name, in: imageModel.bundle, compatibleWith: nil) + if image == nil { + // For static image, use UIImage as defaults + image = PlatformImage(named: name, in: imageModel.bundle, compatibleWith: nil) + } + #endif + context.coordinator.imageLoading.imageName = name + view.wrapped.image = image + } + + private func updateViewForData(_ data: Data, view: AnimatedImageViewWrapper, context: Context) { + var image: PlatformImage? = SDAnimatedImage(data: data, scale: imageModel.scale) + if image == nil { + // For static image, use UIImage as defaults + image = PlatformImage.sd_image(with: data, scale: imageModel.scale) + } + context.coordinator.imageLoading.imageData = data + view.wrapped.image = image + } + + private func updateViewForURL(_ url: URL?, view: AnimatedImageViewWrapper, context: Context) { + // Determine if image already been loaded and URL is match + var shouldLoad: Bool + if url != context.coordinator.imageLoading.imageURL { + // Change the URL, need new loading + shouldLoad = true + context.coordinator.imageLoading.imageURL = url + } else { + // Same URL, check if already loaded + if context.coordinator.imageLoading.isLoading { + shouldLoad = false + } else if let image = context.coordinator.imageLoading.image { + shouldLoad = false + view.wrapped.image = image + } else { + shouldLoad = true + } + } + if shouldLoad { + setupIndicator(view, context: context) + loadImage(view, context: context) + } + } + func updateView(_ view: AnimatedImageViewWrapper, context: Context) { // Refresh image, imageModel is the Source of Truth, switch the type // Although we have Source of Truth, we can check the previous value, to avoid re-generate SDAnimatedImage, which is performance-cost. - if let name = imageModel.name, name != context.coordinator.imageLoading.imageName { - var image: PlatformImage? - #if os(macOS) - image = SDAnimatedImage(named: name, in: imageModel.bundle) - if image == nil { - // For static image, use NSImage as defaults - let bundle = imageModel.bundle ?? .main - image = bundle.image(forResource: name) - } - #else - image = SDAnimatedImage(named: name, in: imageModel.bundle, compatibleWith: nil) - if image == nil { - // For static image, use UIImage as defaults - image = PlatformImage(named: name, in: imageModel.bundle, compatibleWith: nil) - } - #endif - context.coordinator.imageLoading.imageName = name - view.wrapped.image = image - } else if let data = imageModel.data, data != context.coordinator.imageLoading.imageData { - var image: PlatformImage? = SDAnimatedImage(data: data, scale: imageModel.scale) - if image == nil { - // For static image, use UIImage as defaults - image = PlatformImage.sd_image(with: data, scale: imageModel.scale) - } - context.coordinator.imageLoading.imageData = data - view.wrapped.image = image - } else if let url = imageModel.url { - // Determine if image already been loaded and URL is match - var shouldLoad: Bool - if url != context.coordinator.imageLoading.imageURL { - // Change the URL, need new loading - shouldLoad = true - context.coordinator.imageLoading.imageURL = url - } else { - // Same URL, check if already loaded - if context.coordinator.imageLoading.isLoading { - shouldLoad = false - } else if let image = context.coordinator.imageLoading.image { - shouldLoad = false - view.wrapped.image = image - } else { - shouldLoad = true - } - } - if shouldLoad { - setupIndicator(view, context: context) - loadImage(view, context: context) - } + let kind = imageModel.kind + if kind == .name, let name = imageModel.name, name != context.coordinator.imageLoading.imageName { + updateViewForName(name, view: view, context: context) + } else if kind == .data, let data = imageModel.data, data != context.coordinator.imageLoading.imageData { + updateViewForData(data, view: view, context: context) + } else if kind == .url { + updateViewForURL(imageModel.url, view: view, context: context) + } else { + fatalError("Unsupported model kind: \(kind)") } #if os(macOS)