Try to fix the recusive updateView when using AnimatedImage inside `ScrollView/LazyVStack`. Because from iOS 14+, the @Published update will always trigger another `updateUIView`

This commit is contained in:
DreamPiggy 2021-02-22 16:20:43 +08:00
parent cf8d30810d
commit 3a7132a499
2 changed files with 24 additions and 39 deletions

View File

@ -43,6 +43,11 @@ final class AnimatedLoadingModel : ObservableObject, IndicatorReportable {
@Published var image: PlatformImage? // loaded image, note when progressive loading, this will published multiple times with different partial image @Published var image: PlatformImage? // loaded image, note when progressive loading, this will published multiple times with different partial image
@Published var isLoading: Bool = false // whether network is loading or cache is querying, should only be used for indicator binding @Published var isLoading: Bool = false // whether network is loading or cache is querying, should only be used for indicator binding
@Published var progress: Double = 0 // network progress, should only be used for indicator binding @Published var progress: Double = 0 // network progress, should only be used for indicator binding
/// Used for loading status recording to avoid recursive `updateView`. There are 3 types of loading (Name/Data/URL)
@Published var imageName: String?
@Published var imageData: Data?
@Published var imageURL: URL?
} }
/// Completion Handler Binding Object, supports dynamic @State changes /// Completion Handler Binding Object, supports dynamic @State changes
@ -228,15 +233,19 @@ public struct AnimatedImage : PlatformViewRepresentable {
} }
self.imageHandler.progressBlock?(receivedSize, expectedSize) self.imageHandler.progressBlock?(receivedSize, expectedSize)
}) { (image, data, error, cacheType, finished, _) in }) { (image, data, error, cacheType, finished, _) in
// This is a hack because of Xcode 11.3 bug, the @Published does not trigger another `updateUIView` call if #available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) {
// Here I have to use UIKit/AppKit API to triger the same effect (the window change implicitly cause re-render) // Do nothing
if let hostingView = AnimatedImage.findHostingView(from: view) { } else {
if let _ = hostingView.window { // This is a hack because of Xcode 11.3 bug, the @Published does not trigger another `updateUIView` call
#if os(macOS) // Here I have to use UIKit/AppKit API to triger the same effect (the window change implicitly cause re-render)
hostingView.viewDidMoveToWindow() if let hostingView = AnimatedImage.findHostingView(from: view) {
#else if let _ = hostingView.window {
hostingView.didMoveToWindow() #if os(macOS)
#endif hostingView.viewDidMoveToWindow()
#else
hostingView.didMoveToWindow()
#endif
}
} }
} }
self.imageLoading.image = image self.imageLoading.image = image
@ -263,19 +272,20 @@ public struct AnimatedImage : PlatformViewRepresentable {
func updateView(_ view: AnimatedImageViewWrapper, context: Context) { func updateView(_ view: AnimatedImageViewWrapper, context: Context) {
// Refresh image, imageModel is the Source of Truth, switch the type // Refresh image, imageModel is the Source of Truth, switch the type
// Although we have Source of Truth, we can check the previous value, to avoid re-generate SDAnimatedImage, which is performance-cost. // Although we have Source of Truth, we can check the previous value, to avoid re-generate SDAnimatedImage, which is performance-cost.
if let name = imageModel.name, name != view.wrapped.sd_imageName { if let name = imageModel.name, name != imageLoading.imageName {
#if os(macOS) #if os(macOS)
let image = SDAnimatedImage(named: name, in: imageModel.bundle) let image = SDAnimatedImage(named: name, in: imageModel.bundle)
#else #else
let image = SDAnimatedImage(named: name, in: imageModel.bundle, compatibleWith: nil) let image = SDAnimatedImage(named: name, in: imageModel.bundle, compatibleWith: nil)
#endif #endif
view.wrapped.sd_imageName = name imageLoading.imageName = name
view.wrapped.image = image view.wrapped.image = image
} else if let data = imageModel.data, data != view.wrapped.sd_imageData { } else if let data = imageModel.data, data != imageLoading.imageData {
let image = SDAnimatedImage(data: data, scale: imageModel.scale) let image = SDAnimatedImage(data: data, scale: imageModel.scale)
view.wrapped.sd_imageData = data imageLoading.imageData = data
view.wrapped.image = image view.wrapped.image = image
} else if let url = imageModel.url, url != view.wrapped.sd_imageURL { } else if let url = imageModel.url, url != imageLoading.imageURL {
imageLoading.imageURL = url
view.wrapped.sd_imageIndicator = imageConfiguration.indicator view.wrapped.sd_imageIndicator = imageConfiguration.indicator
view.wrapped.sd_imageTransition = imageConfiguration.transition view.wrapped.sd_imageTransition = imageConfiguration.transition
if let placeholderView = imageConfiguration.placeholderView { if let placeholderView = imageConfiguration.placeholderView {

View File

@ -66,31 +66,6 @@ public class AnimatedImageViewWrapper : PlatformView {
} }
} }
/// Store the Animated Image loading state, to avoid re-query duinrg `updateView(_:)` until Source of Truth changes
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension PlatformView {
static private var sd_imageNameKey: Void?
static private var sd_imageDataKey: Void?
var sd_imageName: String? {
get {
objc_getAssociatedObject(self, &PlatformView.sd_imageNameKey) as? String
}
set {
objc_setAssociatedObject(self, &PlatformView.sd_imageNameKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var sd_imageData: Data? {
get {
objc_getAssociatedObject(self, &PlatformView.sd_imageDataKey) as? Data
}
set {
objc_setAssociatedObject(self, &PlatformView.sd_imageDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
/// Use wrapper to solve the `UIProgressView`/`NSProgressIndicator` frame origin NaN crash (SwiftUI's bug) /// Use wrapper to solve the `UIProgressView`/`NSProgressIndicator` frame origin NaN crash (SwiftUI's bug)
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public class ProgressIndicatorWrapper : PlatformView { public class ProgressIndicatorWrapper : PlatformView {