diff --git a/README.md b/README.md index 41c27ffa..51f5e083 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,10 @@ var body: some View { } .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder(UIImage(systemName: "photo")) // Placeholder Image + // Supports ViewBuilder as well + .placeholder { + Circle().foregroundColor(.gray) + } .indicator(SDWebImageActivityIndicator.medium) // Activity Indicator .transition(.fade) // Fade Transition .scaledToFit() // Attention to call it on AnimatedImage, but not `some View` after View Modifier (Swift Protocol Extension method is static dispatched) @@ -176,7 +180,11 @@ var body: some View { AnimatedImage(name: "animation1", isAnimating: $isAnimating)) // Animation control binding .maxBufferSize(.max) .onViewUpdate { view, context in // Advanced native view coordinate + // AppKit tooltip for mouse hover view.toolTip = "Mouseover Tip" + // UIKit advanced content mode + view.contentMode = .topLeft + // Coordinator, used for Cocoa Binding or Delegate method let coordinator = context.coordinator } } @@ -187,6 +195,8 @@ Note: `AnimatedImage` supports both image url or image data for animated image f Note: `AnimatedImage` some methods like `.transition`, `.indicator` and `.aspectRatio` have the same naming as `SwiftUI.View` protocol methods. But the args receive the different type. This is because `AnimatedImage` supports to be used with UIKit/AppKit component and animation. If you find ambiguity, use full type declaration instead of the dot expression syntax. +Note: some of methods on `AnimatedImage` will return `some View`, a new Modified Content. You'll lose the type related modifier method. For this case, you can either reorder the method call, or use Native View in `.onViewUpdate` for rescue. + ```swift var body: some View { AnimatedImage(name: "animation2") // Just for showcase, don't mix them at the same time diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 40dbe751..81a8b885 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -83,6 +83,11 @@ final class AnimatedImageConfiguration: ObservableObject { var indicator: SDWebImageIndicator? var transition: SDWebImageTransition? var placeholder: PlatformImage? + var placeholderView: PlatformView? { + didSet { + oldValue?.removeFromSuperview() + } + } } /// A Image View type to load image from url, data or bundle. Supports animated and static image format. @@ -203,6 +208,11 @@ public struct AnimatedImage : PlatformViewRepresentable { return } self.imageLoading.isLoading = true + if imageModel.webOptions.contains(.delayPlaceholder) { + self.imageConfiguration.placeholderView?.isHidden = true + } else { + self.imageConfiguration.placeholderView?.isHidden = false + } view.wrapped.sd_setImage(with: imageModel.url, placeholderImage: imageConfiguration.placeholder, options: imageModel.webOptions, context: imageModel.webContext, progress: { (receivedSize, expectedSize, _) in let progress: Double if (expectedSize > 0) { @@ -230,8 +240,10 @@ public struct AnimatedImage : PlatformViewRepresentable { self.imageLoading.isLoading = false self.imageLoading.progress = 1 if let image = image { + self.imageConfiguration.placeholderView?.isHidden = true self.imageHandler.successBlock?(image, cacheType) } else { + self.imageConfiguration.placeholderView?.isHidden = false self.imageHandler.failureBlock?(error ?? NSError()) } } @@ -263,6 +275,21 @@ public struct AnimatedImage : PlatformViewRepresentable { } else if let url = imageModel.url, url != view.wrapped.sd_imageURL { view.wrapped.sd_imageIndicator = imageConfiguration.indicator view.wrapped.sd_imageTransition = imageConfiguration.transition + if let placeholderView = imageConfiguration.placeholderView { + placeholderView.removeFromSuperview() + placeholderView.isHidden = true + // Placeholder View should below the Indicator View + if let indicatorView = imageConfiguration.indicator?.indicatorView { + #if os(macOS) + view.wrapped.addSubview(placeholderView, positioned: .below, relativeTo: indicatorView) + #else + view.wrapped.insertSubview(placeholderView, belowSubview: indicatorView) + #endif + } else { + view.wrapped.addSubview(placeholderView) + } + placeholderView.bindFrameToSuperviewBounds() + } loadImage(view, context: context) } @@ -728,8 +755,21 @@ extension AnimatedImage { /// Associate a placeholder when loading image with url /// - Parameter content: A view that describes the placeholder. - public func placeholder(_ placeholder: PlatformImage?) -> AnimatedImage { - self.imageConfiguration.placeholder = placeholder + /// - note: The differences between this and placeholder image, it's that placeholder image replace the image for image view, but this modify the View Hierarchy to overlay the placeholder hosting view + public func placeholder(@ViewBuilder content: () -> T) -> AnimatedImage where T : View { + #if os(macOS) + let hostingView = NSHostingView(rootView: content()) + #else + let hostingView = _UIHostingView(rootView: content()) + #endif + self.imageConfiguration.placeholderView = hostingView + return self + } + + /// Associate a placeholder image when loading image with url + /// - Parameter content: A view that describes the placeholder. + public func placeholder(_ image: PlatformImage?) -> AnimatedImage { + self.imageConfiguration.placeholder = image return self } diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index f72b6742..ae0e9324 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -123,6 +123,9 @@ public final class ImageManager : ObservableObject { manager.loadImage(with: url, options: options, context: context, progress: nil) { (image, data, error, cacheType, finished, imageUrl) in // This will callback immediately self.image = image + if let image = image { + self.successBlock?(image, cacheType) + } } } diff --git a/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift b/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift index 9371e102..681a45cf 100644 --- a/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift +++ b/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift @@ -122,5 +122,21 @@ public class ProgressIndicatorWrapper : PlatformView { addSubview(wrapped) } } +extension PlatformView { + /// Adds constraints to this `UIView` instances `superview` object to make sure this always has the same size as the superview. + /// Please note that this has no effect if its `superview` is `nil` – add this `UIView` instance as a subview before calling this. + func bindFrameToSuperviewBounds() { + guard let superview = self.superview else { + print("Error! `superview` was nil – call `addSubview(view: UIView)` before calling `bindFrameToSuperviewBounds()` to fix this.") + return + } + + self.translatesAutoresizingMaskIntoConstraints = false + self.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0).isActive = true + self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0).isActive = true + self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0).isActive = true + self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: 0).isActive = true + } +} #endif diff --git a/Tests/AnimatedImageTests.swift b/Tests/AnimatedImageTests.swift index d0664f0e..3b27f572 100644 --- a/Tests/AnimatedImageTests.swift +++ b/Tests/AnimatedImageTests.swift @@ -163,6 +163,9 @@ class AnimatedImageTests: XCTestCase { XCTAssertEqual(context.coordinator.userInfo?["foo"] as? String, "bar") } .placeholder(PlatformImage()) + .placeholder { + Circle() + } .indicator(SDWebImageActivityIndicator.medium) // Image .resizable() diff --git a/Tests/WebImageTests.swift b/Tests/WebImageTests.swift index 3a4c6e1d..8be68fbb 100644 --- a/Tests/WebImageTests.swift +++ b/Tests/WebImageTests.swift @@ -82,6 +82,7 @@ class WebImageTests: XCTestCase { .onProgress { _, _ in } + .placeholder(.init(platformImage: PlatformImage())) .placeholder { Circle() }