Skip to content

Commit 5832951

Browse files
authored
Merge pull request #162 from SDWebImage/bugfix_lazyvstack_recursive_update_freeze
Try to fix the recusive updateView when using AnimatedImage inside `ScrollView/LazyVStack`.
2 parents 013a1b9 + 7f8a065 commit 5832951

File tree

3 files changed

+61
-61
lines changed

3 files changed

+61
-61
lines changed

SDWebImageSwiftUI/Classes/AnimatedImage.swift

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ final class AnimatedLoadingModel : ObservableObject, IndicatorReportable {
4343
@Published var image: PlatformImage? // loaded image, note when progressive loading, this will published multiple times with different partial image
4444
@Published var isLoading: Bool = false // whether network is loading or cache is querying, should only be used for indicator binding
4545
@Published var progress: Double = 0 // network progress, should only be used for indicator binding
46+
47+
/// Used for loading status recording to avoid recursive `updateView`. There are 3 types of loading (Name/Data/URL)
48+
@Published var imageName: String?
49+
@Published var imageData: Data?
50+
@Published var imageURL: URL?
4651
}
4752

4853
/// Completion Handler Binding Object, supports dynamic @State changes
@@ -201,12 +206,27 @@ public struct AnimatedImage : PlatformViewRepresentable {
201206
}
202207
#endif
203208

204-
func loadImage(_ view: AnimatedImageViewWrapper, context: Context) {
205-
let operationKey = NSStringFromClass(type(of: view.wrapped))
206-
let currentOperation = view.wrapped.sd_imageLoadOperation(forKey: operationKey)
207-
if currentOperation != nil {
208-
return
209+
func setupIndicator(_ view: AnimatedImageViewWrapper, context: Context) {
210+
view.wrapped.sd_imageIndicator = imageConfiguration.indicator
211+
view.wrapped.sd_imageTransition = imageConfiguration.transition
212+
if let placeholderView = imageConfiguration.placeholderView {
213+
placeholderView.removeFromSuperview()
214+
placeholderView.isHidden = true
215+
// Placeholder View should below the Indicator View
216+
if let indicatorView = imageConfiguration.indicator?.indicatorView {
217+
#if os(macOS)
218+
view.wrapped.addSubview(placeholderView, positioned: .below, relativeTo: indicatorView)
219+
#else
220+
view.wrapped.insertSubview(placeholderView, belowSubview: indicatorView)
221+
#endif
222+
} else {
223+
view.wrapped.addSubview(placeholderView)
224+
}
225+
placeholderView.bindFrameToSuperviewBounds()
209226
}
227+
}
228+
229+
func loadImage(_ view: AnimatedImageViewWrapper, context: Context) {
210230
self.imageLoading.isLoading = true
211231
let options = imageModel.webOptions
212232
if options.contains(.delayPlaceholder) {
@@ -228,15 +248,19 @@ public struct AnimatedImage : PlatformViewRepresentable {
228248
}
229249
self.imageHandler.progressBlock?(receivedSize, expectedSize)
230250
}) { (image, data, error, cacheType, finished, _) in
231-
// This is a hack because of Xcode 11.3 bug, the @Published does not trigger another `updateUIView` call
232-
// Here I have to use UIKit/AppKit API to triger the same effect (the window change implicitly cause re-render)
233-
if let hostingView = AnimatedImage.findHostingView(from: view) {
234-
if let _ = hostingView.window {
235-
#if os(macOS)
236-
hostingView.viewDidMoveToWindow()
237-
#else
238-
hostingView.didMoveToWindow()
239-
#endif
251+
if #available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) {
252+
// Do nothing. on iOS 14's SwiftUI, the @Published will always trigger another `updateUIView` call with new UIView instance.
253+
} else {
254+
// This is a hack because of iOS 13's SwiftUI bug, the @Published does not trigger another `updateUIView` call
255+
// Here I have to use UIKit/AppKit API to triger the same effect (the window change implicitly cause re-render)
256+
if let hostingView = AnimatedImage.findHostingView(from: view) {
257+
if let _ = hostingView.window {
258+
#if os(macOS)
259+
hostingView.viewDidMoveToWindow()
260+
#else
261+
hostingView.didMoveToWindow()
262+
#endif
263+
}
240264
}
241265
}
242266
self.imageLoading.image = image
@@ -263,37 +287,40 @@ public struct AnimatedImage : PlatformViewRepresentable {
263287
func updateView(_ view: AnimatedImageViewWrapper, context: Context) {
264288
// Refresh image, imageModel is the Source of Truth, switch the type
265289
// Although we have Source of Truth, we can check the previous value, to avoid re-generate SDAnimatedImage, which is performance-cost.
266-
if let name = imageModel.name, name != view.wrapped.sd_imageName {
290+
if let name = imageModel.name, name != imageLoading.imageName {
267291
#if os(macOS)
268292
let image = SDAnimatedImage(named: name, in: imageModel.bundle)
269293
#else
270294
let image = SDAnimatedImage(named: name, in: imageModel.bundle, compatibleWith: nil)
271295
#endif
272-
view.wrapped.sd_imageName = name
296+
imageLoading.imageName = name
273297
view.wrapped.image = image
274-
} else if let data = imageModel.data, data != view.wrapped.sd_imageData {
298+
} else if let data = imageModel.data, data != imageLoading.imageData {
275299
let image = SDAnimatedImage(data: data, scale: imageModel.scale)
276-
view.wrapped.sd_imageData = data
300+
imageLoading.imageData = data
277301
view.wrapped.image = image
278-
} else if let url = imageModel.url, url != view.wrapped.sd_imageURL {
279-
view.wrapped.sd_imageIndicator = imageConfiguration.indicator
280-
view.wrapped.sd_imageTransition = imageConfiguration.transition
281-
if let placeholderView = imageConfiguration.placeholderView {
282-
placeholderView.removeFromSuperview()
283-
placeholderView.isHidden = true
284-
// Placeholder View should below the Indicator View
285-
if let indicatorView = imageConfiguration.indicator?.indicatorView {
286-
#if os(macOS)
287-
view.wrapped.addSubview(placeholderView, positioned: .below, relativeTo: indicatorView)
288-
#else
289-
view.wrapped.insertSubview(placeholderView, belowSubview: indicatorView)
290-
#endif
302+
} else if let url = imageModel.url {
303+
// Determine if image already been loaded and URL is match
304+
var shouldLoad: Bool
305+
if url != imageLoading.imageURL {
306+
// Change the URL, need new loading
307+
shouldLoad = true
308+
imageLoading.imageURL = url
309+
} else {
310+
// Same URL, check if already loaded
311+
if imageLoading.isLoading {
312+
shouldLoad = false
313+
} else if let image = imageLoading.image {
314+
shouldLoad = false
315+
view.wrapped.image = image
291316
} else {
292-
view.wrapped.addSubview(placeholderView)
317+
shouldLoad = true
293318
}
294-
placeholderView.bindFrameToSuperviewBounds()
295319
}
296-
loadImage(view, context: context)
320+
if shouldLoad {
321+
setupIndicator(view, context: context)
322+
loadImage(view, context: context)
323+
}
297324
}
298325

299326
#if os(macOS)

SDWebImageSwiftUI/Classes/ImageViewWrapper.swift

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,31 +66,6 @@ public class AnimatedImageViewWrapper : PlatformView {
6666
}
6767
}
6868

69-
70-
/// Store the Animated Image loading state, to avoid re-query duinrg `updateView(_:)` until Source of Truth changes
71-
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
72-
extension PlatformView {
73-
static private var sd_imageNameKey: Void?
74-
static private var sd_imageDataKey: Void?
75-
76-
var sd_imageName: String? {
77-
get {
78-
objc_getAssociatedObject(self, &PlatformView.sd_imageNameKey) as? String
79-
}
80-
set {
81-
objc_setAssociatedObject(self, &PlatformView.sd_imageNameKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
82-
}
83-
}
84-
var sd_imageData: Data? {
85-
get {
86-
objc_getAssociatedObject(self, &PlatformView.sd_imageDataKey) as? Data
87-
}
88-
set {
89-
objc_setAssociatedObject(self, &PlatformView.sd_imageDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
90-
}
91-
}
92-
}
93-
9469
/// Use wrapper to solve the `UIProgressView`/`NSProgressIndicator` frame origin NaN crash (SwiftUI's bug)
9570
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
9671
public class ProgressIndicatorWrapper : PlatformView {

Tests/AnimatedImageTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ class AnimatedImageTests: XCTestCase {
4646
} else {
4747
XCTFail("SDAnimatedImageView.image invalid")
4848
}
49-
XCTAssertEqual(animatedImageView.sd_imageName, imageName)
5049
expectation.fulfill()
5150
self.waitForExpectations(timeout: 5, handler: nil)
5251
ViewHosting.expel()
@@ -64,7 +63,6 @@ class AnimatedImageTests: XCTestCase {
6463
} else {
6564
XCTFail("SDAnimatedImageView.image invalid")
6665
}
67-
XCTAssertEqual(animatedImageView.sd_imageData, imageData)
6866
expectation.fulfill()
6967
self.waitForExpectations(timeout: 5, handler: nil)
7068
ViewHosting.expel()

0 commit comments

Comments
 (0)