Fix the State change behavior again

Using the `StateObject` to check and refresh to the latest status, using `currentURL` and `currentAnimatedImage` to check lazily
This commit is contained in:
DreamPiggy 2022-09-21 23:01:07 +08:00
parent ce5340fd08
commit d281bde037
6 changed files with 87 additions and 21 deletions

View File

@ -34,6 +34,42 @@ extension Indicator where T == ProgressView<EmptyView, EmptyView> {
}
#endif
// Test Switching url using @State
struct ContentView2: View {
@State var imageURLs = [
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_1.jpg",
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_2.jpg",
"http://assets.sbnation.com/assets/2512203/dogflops.gif",
"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"
]
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
@State var imageIndex : Int = 0
var body: some View {
Group {
Text("\(animated ? "AnimatedImage" : "WebImage") - \((imageURLs[imageIndex] as NSString).lastPathComponent)")
Spacer()
if self.animated {
AnimatedImage(url:URL(string: imageURLs[imageIndex]))
.resizable()
.aspectRatio(contentMode: .fit)
} else {
WebImage(url:URL(string: imageURLs[imageIndex]))
.resizable()
.aspectRatio(contentMode: .fit)
}
Spacer()
Button("Next") {
if imageIndex + 1 >= imageURLs.count {
imageIndex = 0
} else {
imageIndex += 1
}
}
Toggle("Switch", isOn: $animated)
}
}
}
struct ContentView: View {
@State var imageURLs = [
"http://assets.sbnation.com/assets/2512203/dogflops.gif",

View File

@ -23,6 +23,10 @@
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
32B79C9528DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
32B79C9628DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
32B79C9728DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
32B79C9828DB40430088C432 /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; };
32B933E523659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
32B933E623659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
32B933E723659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; };
@ -87,6 +91,7 @@
326B8486236335110011BDFB /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
326B848B236335400011BDFB /* ProgressIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = "<group>"; };
326E480923431C0F00C633E9 /* ImageViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewWrapper.swift; sourceTree = "<group>"; };
32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUICompatibility.swift; sourceTree = "<group>"; };
32B933E423659A1900BB7CAD /* Transition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transition.swift; sourceTree = "<group>"; };
32BC086F28D23D35002451BD /* StateObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateObject.swift; sourceTree = "<group>"; };
32BC087028D23D35002451BD /* OnChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnChange.swift; sourceTree = "<group>"; };
@ -233,6 +238,7 @@
32C43DDE22FD54C600BE87F5 /* WebImage.swift */,
32C43DDF22FD54C600BE87F5 /* AnimatedImage.swift */,
32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */,
32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */,
326E480923431C0F00C633E9 /* ImageViewWrapper.swift */,
32D26A012446B546005905DA /* Image.swift */,
);
@ -457,6 +463,7 @@
326B84822363350C0011BDFB /* Indicator.swift in Sources */,
32C43E3222FD5DE100BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
32B79C9528DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
326B8487236335110011BDFB /* ActivityIndicator.swift in Sources */,
32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */,
32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */,
@ -479,6 +486,7 @@
326B84832363350C0011BDFB /* Indicator.swift in Sources */,
32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
32B79C9628DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
326B8488236335110011BDFB /* ActivityIndicator.swift in Sources */,
32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */,
32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */,
@ -501,6 +509,7 @@
326B84842363350C0011BDFB /* Indicator.swift in Sources */,
32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
32B79C9728DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
326B8489236335110011BDFB /* ActivityIndicator.swift in Sources */,
32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */,
32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */,
@ -523,6 +532,7 @@
326B84852363350C0011BDFB /* Indicator.swift in Sources */,
32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */,
326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
32B79C9828DB40430088C432 /* SwiftUICompatibility.swift in Sources */,
326B848A236335110011BDFB /* ActivityIndicator.swift in Sources */,
32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */,
32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */,

View File

@ -27,7 +27,8 @@ public final class ImageManager : ObservableObject {
@Published public var indicatorStatus = IndicatorStatus()
weak var currentOperation: SDWebImageOperation? = nil
var currentURL: URL?
var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)?
var failureBlock: ((Error) -> Void)?
var progressBlock: ((Int, Int) -> Void)?
@ -45,10 +46,12 @@ public final class ImageManager : ObservableObject {
} else {
manager = .shared
}
if currentOperation != nil {
if (currentOperation != nil && currentURL == url) {
return
}
self.indicatorStatus.isLoading = true
currentURL = url
indicatorStatus.isLoading = true
indicatorStatus.progress = 0
currentOperation = manager.loadImage(with: url, options: options, context: context, progress: { [weak self] (receivedSize, expectedSize, _) in
guard let self = self else {
return

View File

@ -45,6 +45,8 @@ public final class ImagePlayer : ObservableObject {
/// Current playing loop count
@Published public var currentLoopCount: UInt = 0
var currentAnimatedImage: (PlatformImage & SDAnimatedImageProvider)?
/// Whether current player is valid for playing. This will check the internal player exist or not
public var isValid: Bool {
player != nil
@ -97,10 +99,11 @@ public final class ImagePlayer : ObservableObject {
/// Setup the player using Animated Image.
/// After setup, you can always check `isValid` status, or call `startPlaying` to play the animation.
/// - Parameter image: animated image
public func setupPlayer(animatedImage: SDAnimatedImageProvider) {
public func setupPlayer(animatedImage: PlatformImage & SDAnimatedImageProvider) {
if isValid {
return
}
currentAnimatedImage = animatedImage
if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
imagePlayer.animationFrameHandler = { [weak self] (index, frame) in
self?.currentFrameIndex = index

View File

@ -84,7 +84,7 @@ extension View {
/// - Returns: Some view
func onPlatformAppear(appear: @escaping () -> Void = {}, disappear: @escaping () -> Void = {}) -> some View {
#if os(iOS) || os(tvOS) || os(macOS)
return self.overlay(PlatformAppear(appearAction: appear, disappearAction: disappear))
return self.background(PlatformAppear(appearAction: appear, disappearAction: disappear))
#else
return self.onAppear(perform: appear).onDisappear(perform: disappear)
#endif

View File

@ -14,8 +14,8 @@ import SDWebImage
final class WebImageModel : ObservableObject {
/// URL image
@Published var url: URL?
@Published var webOptions: SDWebImageOptions = []
@Published var webContext: [SDWebImageContextOption : Any]? = nil
@Published var options: SDWebImageOptions = []
@Published var context: [SDWebImageContextOption : Any]? = nil
}
/// Completion Handler Binding Object, supports dynamic @State changes
@ -61,11 +61,13 @@ public struct WebImage : View {
/// A observed object to pass through the image configuration to player
@ObservedObject var imageConfiguration = WebImageConfiguration()
@ObservedObject var indicatorStatus : IndicatorStatus
// FIXME: Use SwiftUI StateObject and remove onPlatformAppear once drop iOS 13 support
@Backport.StateObject var imagePlayer = ImagePlayer()
// FIXME: Use SwiftUI StateObject and remove onPlatformAppear once drop iOS 13 support
@Backport.StateObject var imageManager = ImageManager()
@Backport.StateObject var imageManager : ImageManager
/// Create a web image with url, placeholder, custom options and context. Optional can support animated image using Binding.
/// - Parameter url: The image url
@ -83,9 +85,12 @@ public struct WebImage : View {
}
let imageModel = WebImageModel()
imageModel.url = url
imageModel.webOptions = options
imageModel.webContext = context
imageModel.options = options
imageModel.context = context
_imageModel = ObservedObject(wrappedValue: imageModel)
let imageManager = ImageManager()
_imageManager = Backport.StateObject(wrappedValue: imageManager)
_indicatorStatus = ObservedObject(wrappedValue: imageManager.indicatorStatus)
}
/// Create a web image with url, placeholder, custom options and context.
@ -98,7 +103,7 @@ public struct WebImage : View {
public var body: some View {
return Group {
if let image = imageManager.image {
if imageManager.image != nil && imageModel.url == imageManager.currentURL {
if isAnimating && !imageManager.isIncremental {
setupPlayer()
.onDisappear {
@ -118,7 +123,7 @@ public struct WebImage : View {
if let currentFrame = imagePlayer.currentFrame {
configure(image: currentFrame)
} else {
configure(image: image)
configure(image: imageManager.image!)
}
}
} else {
@ -127,17 +132,19 @@ public struct WebImage : View {
self.imageManager.successBlock = self.imageHandler.successBlock
self.imageManager.failureBlock = self.imageHandler.failureBlock
self.imageManager.progressBlock = self.imageHandler.progressBlock
// Load remote image when first appear
self.imageManager.load(url: imageModel.url, options: imageModel.webOptions, context: imageModel.webContext)
if (self.imageManager.error == nil) {
// Load remote image when first appear
self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context)
}
guard self.imageConfiguration.retryOnAppear else { return }
// When using prorgessive loading, the new partial image will cause onAppear. Filter this case
if self.imageManager.image == nil && !self.imageManager.isIncremental {
self.imageManager.load(url: imageModel.url, options: imageModel.webOptions, context: imageModel.webContext)
if self.imageManager.error != nil && !self.imageManager.isIncremental {
self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context)
}
}, disappear: {
guard self.imageConfiguration.cancelOnDisappear else { return }
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
if self.imageManager.image == nil && !self.imageManager.isIncremental {
if self.imageManager.error != nil && !self.imageManager.isIncremental {
self.imageManager.cancel()
}
})
@ -196,18 +203,25 @@ public struct WebImage : View {
/// Animated Image Support
func setupPlayer() -> some View {
if let currentFrame = imagePlayer.currentFrame {
if let currentFrame = imagePlayer.currentFrame, imagePlayer.currentAnimatedImage == imageManager.image! {
return configure(image: currentFrame).onAppear {
self.imagePlayer.startPlaying()
}
} else {
return configure(image: imageManager.image!).onAppear {
if let animatedImage = imageManager.image as? SDAnimatedImageProvider {
self.imagePlayer.stopPlaying()
if let animatedImage = imageManager.image as? PlatformImage & SDAnimatedImageProvider {
// Clear previous status
self.imagePlayer.player = nil;
self.imagePlayer.currentFrame = nil;
self.imagePlayer.currentFrameIndex = 0;
self.imagePlayer.currentLoopCount = 0;
self.imagePlayer.customLoopCount = self.imageConfiguration.customLoopCount
self.imagePlayer.maxBufferSize = self.imageConfiguration.maxBufferSize
self.imagePlayer.runLoopMode = self.imageConfiguration.runLoopMode
self.imagePlayer.playbackMode = self.imageConfiguration.playbackMode
self.imagePlayer.playbackRate = self.imageConfiguration.playbackRate
// Setup new player
self.imagePlayer.setupPlayer(animatedImage: animatedImage)
self.imagePlayer.startPlaying()
}
@ -220,7 +234,7 @@ public struct WebImage : View {
// Don't use `Group` because it will trigger `.onAppear` and `.onDisappear` when condition view removed, treat placeholder as an entire component
if let placeholder = placeholder {
// If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :)
if imageModel.webOptions.contains(.delayPlaceholder) && imageManager.error == nil {
if imageModel.options.contains(.delayPlaceholder) && imageManager.error == nil {
return AnyView(configure(image: .empty))
} else {
return placeholder
@ -347,7 +361,7 @@ extension WebImage {
/// Associate a indicator when loading image with url
/// - Parameter indicator: The indicator type, see `Indicator`
public func indicator<T>(_ indicator: Indicator<T>) -> some View where T : View {
return self.modifier(IndicatorViewModifier(status: imageManager.indicatorStatus, indicator: indicator))
return self.modifier(IndicatorViewModifier(status: indicatorStatus, indicator: indicator))
}
/// Associate a indicator when loading image with url, convenient method with block