@@ -15,45 +15,15 @@ import SDWebImage
1515@available ( iOS 14 . 0 , macOS 11 . 0 , tvOS 14 . 0 , watchOS 7 . 0 , * )
1616public final class ImageManager : ObservableObject {
1717 /// loaded image, note when progressive loading, this will published multiple times with different partial image
18- public var image : PlatformImage ? {
19- didSet {
20- DispatchQueue . main. async {
21- self . objectWillChange. send ( )
22- }
23- }
24- }
18+ public var image : PlatformImage ?
2519 /// loaded image data, may be nil if hit from memory cache. This will only published once even on incremental image loading
26- public var imageData : Data ? {
27- didSet {
28- DispatchQueue . main. async {
29- self . objectWillChange. send ( )
30- }
31- }
32- }
20+ public var imageData : Data ?
3321 /// loaded image cache type, .none means from network
34- public var cacheType : SDImageCacheType = . none {
35- didSet {
36- DispatchQueue . main. async {
37- self . objectWillChange. send ( )
38- }
39- }
40- }
22+ public var cacheType : SDImageCacheType = . none
4123 /// loading error, you can grab the error code and reason listed in `SDWebImageErrorDomain`, to provide a user interface about the error reason
42- public var error : Error ? {
43- didSet {
44- DispatchQueue . main. async {
45- self . objectWillChange. send ( )
46- }
47- }
48- }
24+ public var error : Error ?
4925 /// true means during incremental loading
50- public var isIncremental : Bool = false {
51- didSet {
52- DispatchQueue . main. async {
53- self . objectWillChange. send ( )
54- }
55- }
56- }
26+ public var isIncremental : Bool = false
5727 /// A observed object to pass through the image manager loading status to indicator
5828 public var indicatorStatus = IndicatorStatus ( )
5929
@@ -85,46 +55,48 @@ public final class ImageManager : ObservableObject {
8555 self . indicatorStatus. isLoading = true
8656 self . indicatorStatus. progress = 0
8757 currentOperation = manager. loadImage ( with: url, options: options, context: context, progress: { [ weak self] ( receivedSize, expectedSize, _) in
88- // This block may be called in non-main thread
89- guard let self = self else {
90- return
91- }
92- let progress : Double
93- if ( expectedSize > 0 ) {
94- progress = Double ( receivedSize) / Double( expectedSize)
95- } else {
96- progress = 0
97- }
98- self . indicatorStatus. progress = progress
99- if let progressBlock = self . progressBlock {
100- DispatchQueue . main. async {
101- progressBlock ( receivedSize, expectedSize)
58+ // This block may be called in non-main thread — dispatch to main for thread safety
59+ DispatchQueue . main. async { [ weak self] in
60+ guard let self else { return }
61+ let progress : Double
62+ if ( expectedSize > 0 ) {
63+ progress = Double ( receivedSize) / Double( expectedSize)
64+ } else {
65+ progress = 0
10266 }
67+ self . indicatorStatus. progress = progress
68+ self . progressBlock ? ( receivedSize, expectedSize)
10369 }
10470 } ) { [ weak self] ( image, data, error, cacheType, finished, _) in
105- guard let self = self else {
106- return
107- }
108- if let error = error as? SDWebImageError , error. code == . cancelled {
109- // Ignore user cancelled
110- // There are race condition when quick scroll
111- // Indicator modifier disapper and trigger `WebImage.body`
112- // So previous View struct call `onDisappear` and cancel the currentOperation
113- return
114- }
115- withTransaction ( self . transaction) {
116- self . image = image
117- self . error = error
118- self . isIncremental = !finished
119- if finished {
120- self . imageData = data
121- self . cacheType = cacheType
122- self . indicatorStatus. isLoading = false
123- self . indicatorStatus. progress = 1
124- if let image = image {
125- self . successBlock ? ( image, data, cacheType)
126- } else {
127- self . failureBlock ? ( error ?? NSError ( ) )
71+ guard let self else { return }
72+ // Completion may be called on any thread — always dispatch to main so
73+ // withTransaction and all property writes (SwiftUI APIs) are thread-safe.
74+ DispatchQueue . main. async { [ weak self] in
75+ guard let self else { return }
76+ if let error = error as? SDWebImageError , error. code == . cancelled {
77+ // Ignore user cancelled
78+ // There are race condition when quick scroll
79+ // Indicator modifier disapper and trigger `WebImage.body`
80+ // So previous View struct call `onDisappear` and cancel the currentOperation
81+ return
82+ }
83+ // Send once, before any mutation — correct ObservableObject semantics.
84+ // This collapses all property changes into a single SwiftUI render pass.
85+ self . objectWillChange. send ( )
86+ withTransaction ( self . transaction) {
87+ self . image = image
88+ self . error = error
89+ self . isIncremental = !finished
90+ if finished {
91+ self . imageData = data
92+ self . cacheType = cacheType
93+ self . indicatorStatus. isLoading = false
94+ self . indicatorStatus. progress = 1
95+ if let image = image {
96+ self . successBlock ? ( image, data, cacheType)
97+ } else {
98+ self . failureBlock ? ( error ?? NSError ( ) )
99+ }
128100 }
129101 }
130102 }
0 commit comments