diff --git a/SDWebImageSwiftUI.xcodeproj/project.pbxproj b/SDWebImageSwiftUI.xcodeproj/project.pbxproj index 4561eb4..61bba7e 100644 --- a/SDWebImageSwiftUI.xcodeproj/project.pbxproj +++ b/SDWebImageSwiftUI.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 3243AFE62AA37EFF0049A43B /* SwiftUICompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */; }; 3243AFE72AA37EFF0049A43B /* WebImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C43DDE22FD54C600BE87F5 /* WebImage.swift */; }; 3243AFE82AA37EFF0049A43B /* ImagePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32CBA77E25E4D7D800C6A8DC /* ImagePlayer.swift */; }; 3243AFE92AA37EFF0049A43B /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; }; @@ -26,10 +25,6 @@ 326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; }; 326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; }; 329885EE2AA37FCB0071F2BA /* SDWebImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 329885ED2AA37FCB0071F2BA /* SDWebImage.framework */; }; - 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 */; }; @@ -83,7 +78,6 @@ 326B84812363350C0011BDFB /* Indicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Indicator.swift; sourceTree = ""; }; 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewWrapper.swift; sourceTree = ""; }; 329885ED2AA37FCB0071F2BA /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/visionOS/SDWebImage.framework; sourceTree = ""; }; - 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUICompatibility.swift; sourceTree = ""; }; 32B933E423659A1900BB7CAD /* Transition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transition.swift; sourceTree = ""; }; 32C43DCC22FD540D00BE87F5 /* SDWebImageSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SDWebImageSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 32C43DDC22FD54C600BE87F5 /* ImageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageManager.swift; sourceTree = ""; }; @@ -221,7 +215,6 @@ 32C43DDE22FD54C600BE87F5 /* WebImage.swift */, 32C43DDF22FD54C600BE87F5 /* AnimatedImage.swift */, 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */, - 32B79C9428DB40430088C432 /* SwiftUICompatibility.swift */, 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */, 32D26A012446B546005905DA /* Image.swift */, ); @@ -491,7 +484,6 @@ 3243AFEB2AA37EFF0049A43B /* AnimatedImage.swift in Sources */, 3243AFE82AA37EFF0049A43B /* ImagePlayer.swift in Sources */, 3243AFED2AA37EFF0049A43B /* SDWebImageSwiftUI.swift in Sources */, - 3243AFE62AA37EFF0049A43B /* SwiftUICompatibility.swift in Sources */, 3243AFEE2AA37F010049A43B /* Indicator.swift in Sources */, 3243AFEA2AA37EFF0049A43B /* Image.swift in Sources */, ); @@ -507,7 +499,6 @@ 326B84822363350C0011BDFB /* Indicator.swift in Sources */, 32C43E3222FD5DE100BE87F5 /* SDWebImageSwiftUI.swift in Sources */, 326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, - 32B79C9528DB40430088C432 /* SwiftUICompatibility.swift in Sources */, 32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */, 32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */, 32D26A022446B546005905DA /* Image.swift in Sources */, @@ -524,7 +515,6 @@ 326B84832363350C0011BDFB /* Indicator.swift in Sources */, 32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, 326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, - 32B79C9628DB40430088C432 /* SwiftUICompatibility.swift in Sources */, 32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */, 32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */, 32D26A032446B546005905DA /* Image.swift in Sources */, @@ -541,7 +531,6 @@ 326B84842363350C0011BDFB /* Indicator.swift in Sources */, 32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, 326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, - 32B79C9728DB40430088C432 /* SwiftUICompatibility.swift in Sources */, 32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */, 32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */, 32D26A042446B546005905DA /* Image.swift in Sources */, @@ -558,7 +547,6 @@ 326B84852363350C0011BDFB /* Indicator.swift in Sources */, 32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, 326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, - 32B79C9828DB40430088C432 /* SwiftUICompatibility.swift in Sources */, 32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */, 32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */, 32D26A052446B546005905DA /* Image.swift in Sources */, diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index 008b0a3..dd2bb8c 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -15,17 +15,47 @@ import SDWebImage @available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) public final class ImageManager : ObservableObject { /// loaded image, note when progressive loading, this will published multiple times with different partial image - @Published public var image: PlatformImage? + public var image: PlatformImage? { + didSet { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } /// loaded image data, may be nil if hit from memory cache. This will only published once even on incremental image loading - @Published public var imageData: Data? + public var imageData: Data? { + didSet { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } /// loaded image cache type, .none means from network - @Published public var cacheType: SDImageCacheType = .none + public var cacheType: SDImageCacheType = .none { + didSet { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } /// loading error, you can grab the error code and reason listed in `SDWebImageErrorDomain`, to provide a user interface about the error reason - @Published public var error: Error? + public var error: Error? { + didSet { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } /// true means during incremental loading - @Published public var isIncremental: Bool = false + public var isIncremental: Bool = false { + didSet { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } /// A observed object to pass through the image manager loading status to indicator - @Published public var indicatorStatus = IndicatorStatus() + public var indicatorStatus = IndicatorStatus() weak var currentOperation: SDWebImageOperation? = nil @@ -51,8 +81,8 @@ public final class ImageManager : ObservableObject { return } currentURL = url - indicatorStatus.isLoading = true - indicatorStatus.progress = 0 + self.indicatorStatus.isLoading = true + self.indicatorStatus.progress = 0 currentOperation = manager.loadImage(with: url, options: options, context: context, progress: { [weak self] (receivedSize, expectedSize, _) in guard let self = self else { return @@ -63,9 +93,7 @@ public final class ImageManager : ObservableObject { } else { progress = 0 } - DispatchQueue.main.async { - self.indicatorStatus.progress = progress - } + self.indicatorStatus.progress = progress self.progressBlock?(receivedSize, expectedSize) }) { [weak self] (image, data, error, cacheType, finished, _) in guard let self = self else { diff --git a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift index 26f0162..739212e 100644 --- a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift +++ b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift @@ -28,9 +28,21 @@ public struct Indicator where T : View { @available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) public class IndicatorStatus : ObservableObject { /// whether indicator is loading or not - @Published var isLoading: Bool = false + var isLoading: Bool = false { + didSet { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } /// indicator progress, should only be used for indicator binding, value between [0.0, 1.0] - @Published var progress: Double = 0 + var progress: Double = 0 { + didSet { + DispatchQueue.main.async { + self.objectWillChange.send() + } + } + } } /// A implementation detail View Modifier with indicator diff --git a/SDWebImageSwiftUI/Classes/SwiftUICompatibility.swift b/SDWebImageSwiftUI/Classes/SwiftUICompatibility.swift deleted file mode 100644 index cb78446..0000000 --- a/SDWebImageSwiftUI/Classes/SwiftUICompatibility.swift +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This file is part of the SDWebImage package. - * (c) DreamPiggy - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import Foundation -import SwiftUI - -#if !os(watchOS) - -@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) -struct PlatformAppear: PlatformViewRepresentable { - let appearAction: () -> Void - let disappearAction: () -> Void - - #if os(iOS) || os(tvOS) || os(visionOS) - func makeUIView(context: Context) -> some UIView { - let view = PlatformAppearView() - view.appearAction = appearAction - view.disappearAction = disappearAction - return view - } - - func updateUIView(_ uiView: UIViewType, context: Context) {} - #endif - #if os(macOS) - func makeNSView(context: Context) -> some NSView { - let view = PlatformAppearView() - view.appearAction = appearAction - view.disappearAction = disappearAction - return view - } - - func updateNSView(_ nsView: NSViewType, context: Context) {} - #endif -} - -@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) -class PlatformAppearView: PlatformView { - var appearAction: () -> Void = {} - var disappearAction: () -> Void = {} - - #if os(iOS) || os(tvOS) - override func willMove(toWindow newWindow: UIWindow?) { - if newWindow != nil { - DispatchQueue.main.async { - self.appearAction() - } - } else { - DispatchQueue.main.async { - self.disappearAction() - } - } - } - #endif - - #if os(macOS) - override func viewWillMove(toWindow newWindow: NSWindow?) { - if newWindow != nil { - DispatchQueue.main.async { - self.appearAction() - } - } else { - DispatchQueue.main.async { - self.disappearAction() - } - } - } - #endif -} - -#endif - -@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) -extension View { - /// Used UIKit/AppKit behavior to detect the SwiftUI view's visibility. - /// This hack is because of SwiftUI 1.0/2.0 buggy behavior. The built-in `onAppear` and `onDisappear` is so massive on some cases. Where UIKit/AppKit is solid. - /// - Parameters: - /// - appear: The action when view appears - /// - disappear: The action when view disappears - /// - Returns: Some view - func onPlatformAppear(appear: @escaping () -> Void = {}, disappear: @escaping () -> Void = {}) -> some View { - #if os(iOS) || os(tvOS) || os(macOS) - return self.background(PlatformAppear(appearAction: appear, disappearAction: disappear)) - #else - return self.onAppear(perform: appear).onDisappear(perform: disappear) - #endif - } -} diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 4b0d091..19b4779 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -163,26 +163,22 @@ public struct WebImage : View where Content: View { } } else { content((imageManager.error != nil) ? .failure(imageManager.error!) : .empty) - setupPlaceholder() + setupInitialState() // Load Logic - .onPlatformAppear(appear: { - self.setupManager() - if (self.imageManager.error == nil) { - // Load remote image when first appear - self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context) - } + .onAppear { guard self.imageConfiguration.retryOnAppear else { return } // When using prorgessive loading, the new partial image will cause onAppear. Filter this case if self.imageManager.error != nil && !self.imageManager.isIncremental { self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context) } - }, disappear: { + } + .onDisappear { guard self.imageConfiguration.cancelOnDisappear else { return } // When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case if self.imageManager.error != nil && !self.imageManager.isIncremental { self.imageManager.cancel() } - }) + } } } } @@ -328,6 +324,16 @@ public struct WebImage : View where Content: View { } } + /// Initial state management (update when imageModel.url changed) + func setupInitialState() -> some View { + self.setupManager() + if (self.imageManager.error == nil) { + // Load remote image when first appear + self.imageManager.load(url: imageModel.url, options: imageModel.options, context: imageModel.context) + } + return setupPlaceholder() + } + /// Placeholder View Support func setupPlaceholder() -> some View { let result = content((imageManager.error != nil) ? .failure(imageManager.error!) : .empty)