From abd9102f6b4db9d6b07ed6e2fbc7889120ae6078 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 22 Sep 2022 15:03:20 +0800 Subject: [PATCH] Update the readme about when using in List/LazyStack/LazyGrid --- .../SDWebImageSwiftUIDemo/ContentView.swift | 97 +++++++++++-------- README.md | 58 +++++++++-- SDWebImageSwiftUI/Classes/WebImage.swift | 10 +- 3 files changed, 111 insertions(+), 54 deletions(-) diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index fc17b74..a3619c9 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -104,6 +104,58 @@ struct ContentView: View { @State var animated: Bool = false // You can change between WebImage/AnimatedImage @EnvironmentObject var settings: UserSettings + // Used to avoid https://twitter.com/fatbobman/status/1572507700436807683?s=20&t=5rfj6BUza5Jii-ynQatCFA + struct ItemView: View { + @Binding var animated: Bool + @State var url: String + var body: some View { + NavigationLink(destination: DetailView(url: url, animated: self.animated)) { + HStack { + if self.animated { + #if os(macOS) || os(iOS) || os(tvOS) + AnimatedImage(url: URL(string:url), isAnimating: .constant(true)) + .onViewUpdate { view, context in + #if os(macOS) + view.toolTip = url + #endif + } + .indicator(SDWebImageActivityIndicator.medium) + /** + .placeholder(UIImage(systemName: "photo")) + */ + .transition(.fade) + .resizable() + .scaledToFit() + .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) + #else + WebImage(url: URL(string:url), isAnimating: self.$animated) + .resizable() + .indicator(.activity) + .transition(.fade(duration: 0.5)) + .scaledToFit() + .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) + #endif + } else { + WebImage(url: URL(string:url), isAnimating: .constant(true)) + .resizable() + /** + .placeholder { + Image(systemName: "photo") + } + */ + .indicator(.activity) + .transition(.fade(duration: 0.5)) + .scaledToFit() + .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) + } + Text((url as NSString).lastPathComponent) + } + } + .buttonStyle(PlainButtonStyle()) + } + } + + var body: some View { #if os(iOS) return NavigationView { @@ -165,49 +217,8 @@ struct ContentView: View { func contentView() -> some View { List { ForEach(imageURLs, id: \.self) { url in - NavigationLink(destination: DetailView(url: url, animated: self.animated)) { - HStack { - if self.animated { - #if os(macOS) || os(iOS) || os(tvOS) - AnimatedImage(url: URL(string:url), isAnimating: .constant(true)) - .onViewUpdate { view, context in - #if os(macOS) - view.toolTip = url - #endif - } - .indicator(SDWebImageActivityIndicator.medium) - /** - .placeholder(UIImage(systemName: "photo")) - */ - .transition(.fade) - .resizable() - .scaledToFit() - .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) - #else - WebImage(url: URL(string:url), isAnimating: self.$animated) - .resizable() - .indicator(.activity) - .transition(.fade(duration: 0.5)) - .scaledToFit() - .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) - #endif - } else { - WebImage(url: URL(string:url), isAnimating: .constant(true)) - .resizable() - /** - .placeholder { - Image(systemName: "photo") - } - */ - .indicator(.activity) - .transition(.fade(duration: 0.5)) - .scaledToFit() - .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) - } - Text((url as NSString).lastPathComponent) - } - } - .buttonStyle(PlainButtonStyle()) + // Must use top level view instead of inlined view structure + ItemView(animated: $animated, url: url) } .onDelete { indexSet in indexSet.forEach { index in diff --git a/README.md b/README.md index b9007e6..01c39e8 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ It looks familiar like `SDWebImageManager`, but it's built for SwiftUI world, wh ```swift struct MyView : View { - @ObservedObject var imageManager: ImageManager + @ObservedObject var imageManager = ImageManager() var body: some View { // Your custom complicated view graph Group { @@ -281,17 +281,11 @@ struct MyView : View { } } // Trigger image loading when appear - .onAppear { self.imageManager.load() } + .onAppear { self.imageManager.load(url: url) } // Cancel image loading when disappear .onDisappear { self.imageManager.cancel() } } } - -struct MyView_Previews: PreviewProvider { - static var previews: some View { - MyView(imageManager: ImageManager(url: URL(string: "https://via.placeholder.com/200x200.jpg")) - } -} ``` ### Customization and configuration setup @@ -337,6 +331,54 @@ For more information, it's really recommended to check our demo, to learn detail ### Common Problems +#### Using WebImage/AnimatedImage in List/LazyStack/LazyGrid and ForEach + +SwiftUI has a known behavior(bug?) when using stateful view in `List/LazyStack/LazyGrid`. +Only the **Top Level** view can hold its own `@State/@StateObject`, but the sub structure will lose state when scroll out of screen. +However, WebImage/Animated is both stateful. To ensure the state keep in sync even when scroll out of screen. you may use some tricks. + +See more: https://twitter.com/fatbobman/status/1572507700436807683?s=21&t=z4FkAWTMvjsgL-wKdJGreQ + +In short, it's not recommanded to do so: + +```swift +struct ContentView { + @State var imageURLs: [String] + var body: some View { + List { + ForEach(imageURLs, id: \.self) { url in + VStack { + WebImage(url) // The top level is `VStack` + } + } + } + } +} +``` + +instead, using this approach: + +```swift +struct ContentView { + struct BodyView { + @State var url: String + var body: some View { + VStack { + WebImage(url) + } + } + } + @State var imageURLs: [String] + var body: some View { + List { + ForEach(imageURLs, id: \.self) { url in + BodyView(url: url) + } + } + } +} +``` + #### Using Image/WebImage/AnimatedImage in Button/NavigationLink SwiftUI's `Button` apply overlay to its content (except `Text`) by default, this is common mistake to write code like this, which cause strange behavior: diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 350e2dd..5e8f0bc 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -253,16 +253,20 @@ public struct WebImage : View { /// 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 + let result: AnyView if let placeholder = placeholder { // If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :) if imageModel.options.contains(.delayPlaceholder) && imageManager.error == nil { - return AnyView(configure(image: .empty).id(UUID())) // UUID to avoid SwiftUI engine cache the status and does not call `onAppear` + result = AnyView(configure(image: .empty)) } else { - return placeholder + result = placeholder } } else { - return AnyView(configure(image: .empty).id(UUID())) // UUID to avoid SwiftUI engine cache the status and does not call `onAppear` + result = AnyView(configure(image: .empty)) } + // UUID 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 `onAppear`, it should be called to sync with state changes :) + return result.id(UUID()) } }