Merge pull request #94 from SDWebImage/feature_animated_placeholder_view_builder
Feature AnimatedImage placeholder view builder
This commit is contained in:
commit
c6e4400142
10
README.md
10
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
|
||||
|
|
|
@ -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<T>(@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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -163,6 +163,9 @@ class AnimatedImageTests: XCTestCase {
|
|||
XCTAssertEqual(context.coordinator.userInfo?["foo"] as? String, "bar")
|
||||
}
|
||||
.placeholder(PlatformImage())
|
||||
.placeholder {
|
||||
Circle()
|
||||
}
|
||||
.indicator(SDWebImageActivityIndicator.medium)
|
||||
// Image
|
||||
.resizable()
|
||||
|
|
|
@ -82,6 +82,7 @@ class WebImageTests: XCTestCase {
|
|||
.onProgress { _, _ in
|
||||
|
||||
}
|
||||
.placeholder(.init(platformImage: PlatformImage()))
|
||||
.placeholder {
|
||||
Circle()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue