Skip to content

Commit abd9102

Browse files
committed
Update the readme about when using in List/LazyStack/LazyGrid
1 parent d186939 commit abd9102

File tree

3 files changed

+111
-54
lines changed

3 files changed

+111
-54
lines changed

Example/SDWebImageSwiftUIDemo/ContentView.swift

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,58 @@ struct ContentView: View {
104104
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
105105
@EnvironmentObject var settings: UserSettings
106106

107+
// Used to avoid https://twitter.com/fatbobman/status/1572507700436807683?s=20&t=5rfj6BUza5Jii-ynQatCFA
108+
struct ItemView: View {
109+
@Binding var animated: Bool
110+
@State var url: String
111+
var body: some View {
112+
NavigationLink(destination: DetailView(url: url, animated: self.animated)) {
113+
HStack {
114+
if self.animated {
115+
#if os(macOS) || os(iOS) || os(tvOS)
116+
AnimatedImage(url: URL(string:url), isAnimating: .constant(true))
117+
.onViewUpdate { view, context in
118+
#if os(macOS)
119+
view.toolTip = url
120+
#endif
121+
}
122+
.indicator(SDWebImageActivityIndicator.medium)
123+
/**
124+
.placeholder(UIImage(systemName: "photo"))
125+
*/
126+
.transition(.fade)
127+
.resizable()
128+
.scaledToFit()
129+
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
130+
#else
131+
WebImage(url: URL(string:url), isAnimating: self.$animated)
132+
.resizable()
133+
.indicator(.activity)
134+
.transition(.fade(duration: 0.5))
135+
.scaledToFit()
136+
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
137+
#endif
138+
} else {
139+
WebImage(url: URL(string:url), isAnimating: .constant(true))
140+
.resizable()
141+
/**
142+
.placeholder {
143+
Image(systemName: "photo")
144+
}
145+
*/
146+
.indicator(.activity)
147+
.transition(.fade(duration: 0.5))
148+
.scaledToFit()
149+
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
150+
}
151+
Text((url as NSString).lastPathComponent)
152+
}
153+
}
154+
.buttonStyle(PlainButtonStyle())
155+
}
156+
}
157+
158+
107159
var body: some View {
108160
#if os(iOS)
109161
return NavigationView {
@@ -165,49 +217,8 @@ struct ContentView: View {
165217
func contentView() -> some View {
166218
List {
167219
ForEach(imageURLs, id: \.self) { url in
168-
NavigationLink(destination: DetailView(url: url, animated: self.animated)) {
169-
HStack {
170-
if self.animated {
171-
#if os(macOS) || os(iOS) || os(tvOS)
172-
AnimatedImage(url: URL(string:url), isAnimating: .constant(true))
173-
.onViewUpdate { view, context in
174-
#if os(macOS)
175-
view.toolTip = url
176-
#endif
177-
}
178-
.indicator(SDWebImageActivityIndicator.medium)
179-
/**
180-
.placeholder(UIImage(systemName: "photo"))
181-
*/
182-
.transition(.fade)
183-
.resizable()
184-
.scaledToFit()
185-
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
186-
#else
187-
WebImage(url: URL(string:url), isAnimating: self.$animated)
188-
.resizable()
189-
.indicator(.activity)
190-
.transition(.fade(duration: 0.5))
191-
.scaledToFit()
192-
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
193-
#endif
194-
} else {
195-
WebImage(url: URL(string:url), isAnimating: .constant(true))
196-
.resizable()
197-
/**
198-
.placeholder {
199-
Image(systemName: "photo")
200-
}
201-
*/
202-
.indicator(.activity)
203-
.transition(.fade(duration: 0.5))
204-
.scaledToFit()
205-
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
206-
}
207-
Text((url as NSString).lastPathComponent)
208-
}
209-
}
210-
.buttonStyle(PlainButtonStyle())
220+
// Must use top level view instead of inlined view structure
221+
ItemView(animated: $animated, url: url)
211222
}
212223
.onDelete { indexSet in
213224
indexSet.forEach { index in

README.md

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ It looks familiar like `SDWebImageManager`, but it's built for SwiftUI world, wh
270270

271271
```swift
272272
struct MyView : View {
273-
@ObservedObject var imageManager: ImageManager
273+
@ObservedObject var imageManager = ImageManager()
274274
var body: some View {
275275
// Your custom complicated view graph
276276
Group {
@@ -281,17 +281,11 @@ struct MyView : View {
281281
}
282282
}
283283
// Trigger image loading when appear
284-
.onAppear { self.imageManager.load() }
284+
.onAppear { self.imageManager.load(url: url) }
285285
// Cancel image loading when disappear
286286
.onDisappear { self.imageManager.cancel() }
287287
}
288288
}
289-
290-
struct MyView_Previews: PreviewProvider {
291-
static var previews: some View {
292-
MyView(imageManager: ImageManager(url: URL(string: "https://via.placeholder.com/200x200.jpg"))
293-
}
294-
}
295289
```
296290

297291
### Customization and configuration setup
@@ -337,6 +331,54 @@ For more information, it's really recommended to check our demo, to learn detail
337331

338332
### Common Problems
339333

334+
#### Using WebImage/AnimatedImage in List/LazyStack/LazyGrid and ForEach
335+
336+
SwiftUI has a known behavior(bug?) when using stateful view in `List/LazyStack/LazyGrid`.
337+
Only the **Top Level** view can hold its own `@State/@StateObject`, but the sub structure will lose state when scroll out of screen.
338+
However, WebImage/Animated is both stateful. To ensure the state keep in sync even when scroll out of screen. you may use some tricks.
339+
340+
See more: https://twitter.com/fatbobman/status/1572507700436807683?s=21&t=z4FkAWTMvjsgL-wKdJGreQ
341+
342+
In short, it's not recommanded to do so:
343+
344+
```swift
345+
struct ContentView {
346+
@State var imageURLs: [String]
347+
var body: some View {
348+
List {
349+
ForEach(imageURLs, id: \.self) { url in
350+
VStack {
351+
WebImage(url) // The top level is `VStack`
352+
}
353+
}
354+
}
355+
}
356+
}
357+
```
358+
359+
instead, using this approach:
360+
361+
```swift
362+
struct ContentView {
363+
struct BodyView {
364+
@State var url: String
365+
var body: some View {
366+
VStack {
367+
WebImage(url)
368+
}
369+
}
370+
}
371+
@State var imageURLs: [String]
372+
var body: some View {
373+
List {
374+
ForEach(imageURLs, id: \.self) { url in
375+
BodyView(url: url)
376+
}
377+
}
378+
}
379+
}
380+
```
381+
340382
#### Using Image/WebImage/AnimatedImage in Button/NavigationLink
341383

342384
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:

SDWebImageSwiftUI/Classes/WebImage.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,20 @@ public struct WebImage : View {
253253
/// Placeholder View Support
254254
func setupPlaceholder() -> some View {
255255
// Don't use `Group` because it will trigger `.onAppear` and `.onDisappear` when condition view removed, treat placeholder as an entire component
256+
let result: AnyView
256257
if let placeholder = placeholder {
257258
// If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :)
258259
if imageModel.options.contains(.delayPlaceholder) && imageManager.error == nil {
259-
return AnyView(configure(image: .empty).id(UUID())) // UUID to avoid SwiftUI engine cache the status and does not call `onAppear`
260+
result = AnyView(configure(image: .empty))
260261
} else {
261-
return placeholder
262+
result = placeholder
262263
}
263264
} else {
264-
return AnyView(configure(image: .empty).id(UUID())) // UUID to avoid SwiftUI engine cache the status and does not call `onAppear`
265+
result = AnyView(configure(image: .empty))
265266
}
267+
// UUID to avoid SwiftUI engine cache the status, and does not call `onAppear` when placeholder not changed (See `ContentView.swift/ContentView2` case)
268+
// Because we load the image url in `onAppear`, it should be called to sync with state changes :)
269+
return result.id(UUID())
266270
}
267271
}
268272

0 commit comments

Comments
 (0)