Skip to content

Commit d281bde

Browse files
committed
Fix the State change behavior again
Using the `StateObject` to check and refresh to the latest status, using `currentURL` and `currentAnimatedImage` to check lazily
1 parent ce5340f commit d281bde

File tree

6 files changed

+87
-21
lines changed

6 files changed

+87
-21
lines changed

Example/SDWebImageSwiftUIDemo/ContentView.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,42 @@ extension Indicator where T == ProgressView<EmptyView, EmptyView> {
3434
}
3535
#endif
3636

37+
// Test Switching url using @State
38+
struct ContentView2: View {
39+
@State var imageURLs = [
40+
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_1.jpg",
41+
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_2.jpg",
42+
"http://assets.sbnation.com/assets/2512203/dogflops.gif",
43+
"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"
44+
]
45+
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
46+
@State var imageIndex : Int = 0
47+
var body: some View {
48+
Group {
49+
Text("\(animated ? "AnimatedImage" : "WebImage") - \((imageURLs[imageIndex] as NSString).lastPathComponent)")
50+
Spacer()
51+
if self.animated {
52+
AnimatedImage(url:URL(string: imageURLs[imageIndex]))
53+
.resizable()
54+
.aspectRatio(contentMode: .fit)
55+
} else {
56+
WebImage(url:URL(string: imageURLs[imageIndex]))
57+
.resizable()
58+
.aspectRatio(contentMode: .fit)
59+
}
60+
Spacer()
61+
Button("Next") {
62+
if imageIndex + 1 >= imageURLs.count {
63+
imageIndex = 0
64+
} else {
65+
imageIndex += 1
66+
}
67+
}
68+
Toggle("Switch", isOn: $animated)
69+
}
70+
}
71+
}
72+
3773
struct ContentView: View {
3874
@State var imageURLs = [
3975
"http://assets.sbnation.com/assets/2512203/dogflops.gif",

SDWebImageSwiftUI.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
2424
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
2525
326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
26+
32B79C9528DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
27+
32B79C9628DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
28+
32B79C9728DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
29+
32B79C9828DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
2630
32B933E523659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
2731
32B933E623659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
2832
32B933E723659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
@@ -87,6 +91,7 @@
8791
326B8486236335110011BDFB /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
8892
326B848B236335400011BDFB /* ProgressIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = "<group>"; };
8993
326E480923431C0F00C633E9 /* ImageViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewWrapper.swift; sourceTree = "<group>"; };
94+
32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUICompatibility.swift; sourceTree = "<group>"; };
9095
32B933E423659A1900BB7CAD /* Transition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transition.swift; sourceTree = "<group>"; };
9196
32BC086F28D23D35002451BD /* StateObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateObject.swift; sourceTree = "<group>"; };
9297
32BC087028D23D35002451BD /* OnChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnChange.swift; sourceTree = "<group>"; };
@@ -233,6 +238,7 @@
233238
32C43DDE22FD54C600BE87F5 /* WebImage.swift */,
234239
32C43DDF22FD54C600BE87F5 /* AnimatedImage.swift */,
235240
32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */,
241+
32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */,
236242
326E480923431C0F00C633E9 /* ImageViewWrapper.swift */,
237243
32D26A012446B546005905DA /* Image.swift */,
238244
);
@@ -457,6 +463,7 @@
457463
326B84822363350C0011BDFB /* Indicator.swift in Sources */,
458464
32C43E3222FD5DE100BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
459465
326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
466+
32B79C9528DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
460467
326B8487236335110011BDFB /* ActivityIndicator.swift in Sources */,
461468
32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */,
462469
32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */,
@@ -479,6 +486,7 @@
479486
326B84832363350C0011BDFB /* Indicator.swift in Sources */,
480487
32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
481488
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
489+
32B79C9628DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
482490
326B8488236335110011BDFB /* ActivityIndicator.swift in Sources */,
483491
32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */,
484492
32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */,
@@ -501,6 +509,7 @@
501509
326B84842363350C0011BDFB /* Indicator.swift in Sources */,
502510
32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
503511
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
512+
32B79C9728DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
504513
326B8489236335110011BDFB /* ActivityIndicator.swift in Sources */,
505514
32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */,
506515
32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */,
@@ -523,6 +532,7 @@
523532
326B84852363350C0011BDFB /* Indicator.swift in Sources */,
524533
32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
525534
326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
535+
32B79C9828DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
526536
326B848A236335110011BDFB /* ActivityIndicator.swift in Sources */,
527537
32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */,
528538
32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */,

SDWebImageSwiftUI/Classes/ImageManager.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public final class ImageManager : ObservableObject {
2727
@Published public var indicatorStatus = IndicatorStatus()
2828

2929
weak var currentOperation: SDWebImageOperation? = nil
30-
30+
31+
var currentURL: URL?
3132
var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)?
3233
var failureBlock: ((Error) -> Void)?
3334
var progressBlock: ((Int, Int) -> Void)?
@@ -45,10 +46,12 @@ public final class ImageManager : ObservableObject {
4546
} else {
4647
manager = .shared
4748
}
48-
if currentOperation != nil {
49+
if (currentOperation != nil && currentURL == url) {
4950
return
5051
}
51-
self.indicatorStatus.isLoading = true
52+
currentURL = url
53+
indicatorStatus.isLoading = true
54+
indicatorStatus.progress = 0
5255
currentOperation = manager.loadImage(with: url, options: options, context: context, progress: { [weak self] (receivedSize, expectedSize, _) in
5356
guard let self = self else {
5457
return

SDWebImageSwiftUI/Classes/ImagePlayer.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public final class ImagePlayer : ObservableObject {
4545
/// Current playing loop count
4646
@Published public var currentLoopCount: UInt = 0
4747

48+
var currentAnimatedImage: (PlatformImage & SDAnimatedImageProvider)?
49+
4850
/// Whether current player is valid for playing. This will check the internal player exist or not
4951
public var isValid: Bool {
5052
player != nil
@@ -97,10 +99,11 @@ public final class ImagePlayer : ObservableObject {
9799
/// Setup the player using Animated Image.
98100
/// After setup, you can always check `isValid` status, or call `startPlaying` to play the animation.
99101
/// - Parameter image: animated image
100-
public func setupPlayer(animatedImage: SDAnimatedImageProvider) {
102+
public func setupPlayer(animatedImage: PlatformImage & SDAnimatedImageProvider) {
101103
if isValid {
102104
return
103105
}
106+
currentAnimatedImage = animatedImage
104107
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
105108
imagePlayer.animationFrameHandler = { [weak self] (index, frame) in
106109
self?.currentFrameIndex = index

SDWebImageSwiftUI/Classes/SwiftUICompatibility.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ extension View {
8484
/// - Returns: Some view
8585
func onPlatformAppear(appear: @escaping () -> Void = {}, disappear: @escaping () -> Void = {}) -> some View {
8686
#if os(iOS) || os(tvOS) || os(macOS)
87-
return self.overlay(PlatformAppear(appearAction: appear, disappearAction: disappear))
87+
return self.background(PlatformAppear(appearAction: appear, disappearAction: disappear))
8888
#else
8989
return self.onAppear(perform: appear).onDisappear(perform: disappear)
9090
#endif

SDWebImageSwiftUI/Classes/WebImage.swift

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import SDWebImage
1414
final class WebImageModel : ObservableObject {
1515
/// URL image
1616
@Published var url: URL?
17-
@Published var webOptions: SDWebImageOptions = []
18-
@Published var webContext: [SDWebImageContextOption : Any]? = nil
17+
@Published var options: SDWebImageOptions = []
18+
@Published var context: [SDWebImageContextOption : Any]? = nil
1919
}
2020

2121
/// Completion Handler Binding Object, supports dynamic @State changes
@@ -61,11 +61,13 @@ public struct WebImage : View {
6161
/// A observed object to pass through the image configuration to player
6262
@ObservedObject var imageConfiguration = WebImageConfiguration()
6363

64+
@ObservedObject var indicatorStatus : IndicatorStatus
65+
6466
// FIXME: Use SwiftUI StateObject and remove onPlatformAppear once drop iOS 13 support
6567
@Backport.StateObject var imagePlayer = ImagePlayer()
6668

6769
// FIXME: Use SwiftUI StateObject and remove onPlatformAppear once drop iOS 13 support
68-
@Backport.StateObject var imageManager = ImageManager()
70+
@Backport.StateObject var imageManager : ImageManager
6971

7072
/// Create a web image with url, placeholder, custom options and context. Optional can support animated image using Binding.
7173
/// - Parameter url: The image url
@@ -83,9 +85,12 @@ public struct WebImage : View {
8385
}
8486
let imageModel = WebImageModel()
8587
imageModel.url = url
86-
imageModel.webOptions = options
87-
imageModel.webContext = context
88+
imageModel.options = options
89+
imageModel.context = context
8890
_imageModel = ObservedObject(wrappedValue: imageModel)
91+
let imageManager = ImageManager()
92+
_imageManager = Backport.StateObject(wrappedValue: imageManager)
93+
_indicatorStatus = ObservedObject(wrappedValue: imageManager.indicatorStatus)
8994
}
9095

9196
/// Create a web image with url, placeholder, custom options and context.
@@ -98,7 +103,7 @@ public struct WebImage : View {
98103

99104
public var body: some View {
100105
return Group {
101-
if let image = imageManager.image {
106+
if imageManager.image != nil && imageModel.url == imageManager.currentURL {
102107
if isAnimating && !imageManager.isIncremental {
103108
setupPlayer()
104109
.onDisappear {
@@ -118,7 +123,7 @@ public struct WebImage : View {
118123
if let currentFrame = imagePlayer.currentFrame {
119124
configure(image: currentFrame)
120125
} else {
121-
configure(image: image)
126+
configure(image: imageManager.image!)
122127
}
123128
}
124129
} else {
@@ -127,17 +132,19 @@ public struct WebImage : View {
127132
self.imageManager.successBlock = self.imageHandler.successBlock
128133
self.imageManager.failureBlock = self.imageHandler.failureBlock
129134
self.imageManager.progressBlock = self.imageHandler.progressBlock
130-
// Load remote image when first appear
131-
self.imageManager.load(url: imageModel.url, options: imageModel.webOptions, context: imageModel.webContext)
135+
if (self.imageManager.error == nil) {
136+
// Load remote image when first appear
137+
self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context)
138+
}
132139
guard self.imageConfiguration.retryOnAppear else { return }
133140
// When using prorgessive loading, the new partial image will cause onAppear. Filter this case
134-
if self.imageManager.image == nil && !self.imageManager.isIncremental {
135-
self.imageManager.load(url: imageModel.url, options: imageModel.webOptions, context: imageModel.webContext)
141+
if self.imageManager.error != nil && !self.imageManager.isIncremental {
142+
self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context)
136143
}
137144
}, disappear: {
138145
guard self.imageConfiguration.cancelOnDisappear else { return }
139146
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
140-
if self.imageManager.image == nil && !self.imageManager.isIncremental {
147+
if self.imageManager.error != nil && !self.imageManager.isIncremental {
141148
self.imageManager.cancel()
142149
}
143150
})
@@ -196,18 +203,25 @@ public struct WebImage : View {
196203

197204
/// Animated Image Support
198205
func setupPlayer() -> some View {
199-
if let currentFrame = imagePlayer.currentFrame {
206+
if let currentFrame = imagePlayer.currentFrame, imagePlayer.currentAnimatedImage == imageManager.image! {
200207
return configure(image: currentFrame).onAppear {
201208
self.imagePlayer.startPlaying()
202209
}
203210
} else {
204211
return configure(image: imageManager.image!).onAppear {
205-
if let animatedImage = imageManager.image as? SDAnimatedImageProvider {
212+
self.imagePlayer.stopPlaying()
213+
if let animatedImage = imageManager.image as? PlatformImage & SDAnimatedImageProvider {
214+
// Clear previous status
215+
self.imagePlayer.player = nil;
216+
self.imagePlayer.currentFrame = nil;
217+
self.imagePlayer.currentFrameIndex = 0;
218+
self.imagePlayer.currentLoopCount = 0;
206219
self.imagePlayer.customLoopCount = self.imageConfiguration.customLoopCount
207220
self.imagePlayer.maxBufferSize = self.imageConfiguration.maxBufferSize
208221
self.imagePlayer.runLoopMode = self.imageConfiguration.runLoopMode
209222
self.imagePlayer.playbackMode = self.imageConfiguration.playbackMode
210223
self.imagePlayer.playbackRate = self.imageConfiguration.playbackRate
224+
// Setup new player
211225
self.imagePlayer.setupPlayer(animatedImage: animatedImage)
212226
self.imagePlayer.startPlaying()
213227
}
@@ -220,7 +234,7 @@ public struct WebImage : View {
220234
// Don't use `Group` because it will trigger `.onAppear` and `.onDisappear` when condition view removed, treat placeholder as an entire component
221235
if let placeholder = placeholder {
222236
// If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :)
223-
if imageModel.webOptions.contains(.delayPlaceholder) && imageManager.error == nil {
237+
if imageModel.options.contains(.delayPlaceholder) && imageManager.error == nil {
224238
return AnyView(configure(image: .empty))
225239
} else {
226240
return placeholder
@@ -347,7 +361,7 @@ extension WebImage {
347361
/// Associate a indicator when loading image with url
348362
/// - Parameter indicator: The indicator type, see `Indicator`
349363
public func indicator<T>(_ indicator: Indicator<T>) -> some View where T : View {
350-
return self.modifier(IndicatorViewModifier(status: imageManager.indicatorStatus, indicator: indicator))
364+
return self.modifier(IndicatorViewModifier(status: indicatorStatus, indicator: indicator))
351365
}
352366

353367
/// Associate a indicator when loading image with url, convenient method with block

0 commit comments

Comments
 (0)