Skip to content

Commit 5419918

Browse files
authored
Merge pull request #304 from SDWebImage/bugfix/nil_url
Fix the issue for WebImage/AnimatedImage when url is nil will not cause the reloading
2 parents 8e445db + 6ba07e3 commit 5419918

File tree

3 files changed

+120
-48
lines changed

3 files changed

+120
-48
lines changed

Example/SDWebImageSwiftUIDemo/ContentView.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,43 @@ class UserSettings: ObservableObject {
1717
#endif
1818
}
1919

20+
// Test Switching nil url
21+
struct ContentView3: View {
22+
@State var isOn = false
23+
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
24+
25+
var url: URL? {
26+
if isOn {
27+
.init(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/1024px-Google_%22G%22_logo.svg.png")
28+
} else {
29+
nil
30+
}
31+
}
32+
33+
var body: some View {
34+
VStack {
35+
Text("\(animated ? "AnimatedImage" : "WebImage")")
36+
Spacer()
37+
if animated {
38+
AnimatedImage(url: url)
39+
.resizable()
40+
.scaledToFit()
41+
.frame(width: 100, height: 100)
42+
} else {
43+
WebImage(url: url)
44+
.resizable()
45+
.scaledToFit()
46+
.frame(width: 100, height: 100)
47+
}
48+
Button("Toggle \(isOn ? "nil" : "valid") URL") {
49+
isOn.toggle()
50+
}
51+
Spacer()
52+
Toggle("Switch", isOn: $animated)
53+
}
54+
}
55+
}
56+
2057
// Test Switching url using @State
2158
struct ContentView2: View {
2259
@State var imageURLs = [

SDWebImageSwiftUI/Classes/AnimatedImage.swift

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ public final class AnimatedImageCoordinator: NSObject {
2727
/// Data Binding Object, only properties in this object can support changes from user with @State and refresh
2828
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
2929
final class AnimatedImageModel : ObservableObject {
30+
enum Kind {
31+
case url
32+
case data
33+
case name
34+
case unknown
35+
}
36+
var kind: Kind = .unknown
3037
/// URL image
3138
@Published var url: URL?
3239
@Published var webOptions: SDWebImageOptions = []
@@ -123,6 +130,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
123130
/// - Parameter isAnimating: The binding for animation control
124131
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), placeholderImage: PlatformImage? = nil) {
125132
let imageModel = AnimatedImageModel()
133+
imageModel.kind = .url
126134
imageModel.url = url
127135
imageModel.webOptions = options
128136
imageModel.webContext = context
@@ -138,6 +146,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
138146
/// - Parameter isAnimating: The binding for animation control
139147
public init<T>(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), @ViewBuilder placeholder: @escaping () -> T) where T : View {
140148
let imageModel = AnimatedImageModel()
149+
imageModel.kind = .url
141150
imageModel.url = url
142151
imageModel.webOptions = options
143152
imageModel.webContext = context
@@ -157,6 +166,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
157166
/// - Parameter isAnimating: The binding for animation control
158167
public init(name: String, bundle: Bundle? = nil, isAnimating: Binding<Bool> = .constant(true)) {
159168
let imageModel = AnimatedImageModel()
169+
imageModel.kind = .name
160170
imageModel.name = name
161171
imageModel.bundle = bundle
162172
self.init(imageModel: imageModel, isAnimating: isAnimating)
@@ -168,6 +178,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
168178
/// - Parameter isAnimating: The binding for animation control
169179
public init(data: Data, scale: CGFloat = 1, isAnimating: Binding<Bool> = .constant(true)) {
170180
let imageModel = AnimatedImageModel()
181+
imageModel.kind = .data
171182
imageModel.data = data
172183
imageModel.scale = scale
173184
self.init(imageModel: imageModel, isAnimating: isAnimating)
@@ -275,57 +286,72 @@ public struct AnimatedImage : PlatformViewRepresentable {
275286
return view
276287
}
277288

289+
private func updateViewForName(_ name: String, view: AnimatedImageViewWrapper, context: Context) {
290+
var image: PlatformImage?
291+
#if os(macOS)
292+
image = SDAnimatedImage(named: name, in: imageModel.bundle)
293+
if image == nil {
294+
// For static image, use NSImage as defaults
295+
let bundle = imageModel.bundle ?? .main
296+
image = bundle.image(forResource: name)
297+
}
298+
#else
299+
image = SDAnimatedImage(named: name, in: imageModel.bundle, compatibleWith: nil)
300+
if image == nil {
301+
// For static image, use UIImage as defaults
302+
image = PlatformImage(named: name, in: imageModel.bundle, compatibleWith: nil)
303+
}
304+
#endif
305+
context.coordinator.imageLoading.imageName = name
306+
view.wrapped.image = image
307+
}
308+
309+
private func updateViewForData(_ data: Data, view: AnimatedImageViewWrapper, context: Context) {
310+
var image: PlatformImage? = SDAnimatedImage(data: data, scale: imageModel.scale)
311+
if image == nil {
312+
// For static image, use UIImage as defaults
313+
image = PlatformImage.sd_image(with: data, scale: imageModel.scale)
314+
}
315+
context.coordinator.imageLoading.imageData = data
316+
view.wrapped.image = image
317+
}
318+
319+
private func updateViewForURL(_ url: URL?, view: AnimatedImageViewWrapper, context: Context) {
320+
// Determine if image already been loaded and URL is match
321+
var shouldLoad: Bool
322+
if url != context.coordinator.imageLoading.imageURL {
323+
// Change the URL, need new loading
324+
shouldLoad = true
325+
context.coordinator.imageLoading.imageURL = url
326+
} else {
327+
// Same URL, check if already loaded
328+
if context.coordinator.imageLoading.isLoading {
329+
shouldLoad = false
330+
} else if let image = context.coordinator.imageLoading.image {
331+
shouldLoad = false
332+
view.wrapped.image = image
333+
} else {
334+
shouldLoad = true
335+
}
336+
}
337+
if shouldLoad {
338+
setupIndicator(view, context: context)
339+
loadImage(view, context: context)
340+
}
341+
}
342+
278343
func updateView(_ view: AnimatedImageViewWrapper, context: Context) {
279344
// Refresh image, imageModel is the Source of Truth, switch the type
280345
// Although we have Source of Truth, we can check the previous value, to avoid re-generate SDAnimatedImage, which is performance-cost.
281-
if let name = imageModel.name, name != context.coordinator.imageLoading.imageName {
282-
var image: PlatformImage?
283-
#if os(macOS)
284-
image = SDAnimatedImage(named: name, in: imageModel.bundle)
285-
if image == nil {
286-
// For static image, use NSImage as defaults
287-
let bundle = imageModel.bundle ?? .main
288-
image = bundle.image(forResource: name)
289-
}
290-
#else
291-
image = SDAnimatedImage(named: name, in: imageModel.bundle, compatibleWith: nil)
292-
if image == nil {
293-
// For static image, use UIImage as defaults
294-
image = PlatformImage(named: name, in: imageModel.bundle, compatibleWith: nil)
295-
}
296-
#endif
297-
context.coordinator.imageLoading.imageName = name
298-
view.wrapped.image = image
299-
} else if let data = imageModel.data, data != context.coordinator.imageLoading.imageData {
300-
var image: PlatformImage? = SDAnimatedImage(data: data, scale: imageModel.scale)
301-
if image == nil {
302-
// For static image, use UIImage as defaults
303-
image = PlatformImage.sd_image(with: data, scale: imageModel.scale)
304-
}
305-
context.coordinator.imageLoading.imageData = data
306-
view.wrapped.image = image
307-
} else if let url = imageModel.url {
308-
// Determine if image already been loaded and URL is match
309-
var shouldLoad: Bool
310-
if url != context.coordinator.imageLoading.imageURL {
311-
// Change the URL, need new loading
312-
shouldLoad = true
313-
context.coordinator.imageLoading.imageURL = url
314-
} else {
315-
// Same URL, check if already loaded
316-
if context.coordinator.imageLoading.isLoading {
317-
shouldLoad = false
318-
} else if let image = context.coordinator.imageLoading.image {
319-
shouldLoad = false
320-
view.wrapped.image = image
321-
} else {
322-
shouldLoad = true
323-
}
324-
}
325-
if shouldLoad {
326-
setupIndicator(view, context: context)
327-
loadImage(view, context: context)
328-
}
346+
let kind = imageModel.kind
347+
if kind == .name, let name = imageModel.name, name != context.coordinator.imageLoading.imageName {
348+
updateViewForName(name, view: view, context: context)
349+
} else if kind == .data, let data = imageModel.data, data != context.coordinator.imageLoading.imageData {
350+
updateViewForData(data, view: view, context: context)
351+
} else if kind == .url {
352+
updateViewForURL(imageModel.url, view: view, context: context)
353+
} else {
354+
fatalError("Unsupported model kind: \(kind)")
329355
}
330356

331357
#if os(macOS)

SDWebImageSwiftUI/Classes/WebImage.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ public struct WebImage<Content> : View where Content: View {
163163
}
164164
} else {
165165
content((imageManager.error != nil) ? .failure(imageManager.error!) : .empty)
166+
setupPlaceholder()
166167
// Load Logic
167168
.onPlatformAppear(appear: {
168169
self.setupManager()
@@ -326,6 +327,14 @@ public struct WebImage<Content> : View where Content: View {
326327
}
327328
}
328329
}
330+
331+
/// Placeholder View Support
332+
func setupPlaceholder() -> some View {
333+
let result = content((imageManager.error != nil) ? .failure(imageManager.error!) : .empty)
334+
// Custom ID to avoid SwiftUI engine cache the status, and does not call `onAppear` when placeholder not changed (See `ContentView.swift/ContentView2` case)
335+
// Because we load the image url in placeholder's `onAppear`, it should be called to sync with state changes :)
336+
return result.id(imageModel.url)
337+
}
329338
}
330339

331340
// Layout

0 commit comments

Comments
 (0)