Skip to content

Commit c6e4400

Browse files
authored
Merge pull request #94 from SDWebImage/feature_animated_placeholder_view_builder
Feature AnimatedImage placeholder view builder
2 parents 7458f13 + 63201c7 commit c6e4400

File tree

6 files changed

+75
-2
lines changed

6 files changed

+75
-2
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ var body: some View {
163163
}
164164
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
165165
.placeholder(UIImage(systemName: "photo")) // Placeholder Image
166+
// Supports ViewBuilder as well
167+
.placeholder {
168+
Circle().foregroundColor(.gray)
169+
}
166170
.indicator(SDWebImageActivityIndicator.medium) // Activity Indicator
167171
.transition(.fade) // Fade Transition
168172
.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 {
176180
AnimatedImage(name: "animation1", isAnimating: $isAnimating)) // Animation control binding
177181
.maxBufferSize(.max)
178182
.onViewUpdate { view, context in // Advanced native view coordinate
183+
// AppKit tooltip for mouse hover
179184
view.toolTip = "Mouseover Tip"
185+
// UIKit advanced content mode
186+
view.contentMode = .topLeft
187+
// Coordinator, used for Cocoa Binding or Delegate method
180188
let coordinator = context.coordinator
181189
}
182190
}
@@ -187,6 +195,8 @@ Note: `AnimatedImage` supports both image url or image data for animated image f
187195

188196
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.
189197

198+
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.
199+
190200
```swift
191201
var body: some View {
192202
AnimatedImage(name: "animation2") // Just for showcase, don't mix them at the same time

SDWebImageSwiftUI/Classes/AnimatedImage.swift

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ final class AnimatedImageConfiguration: ObservableObject {
8383
var indicator: SDWebImageIndicator?
8484
var transition: SDWebImageTransition?
8585
var placeholder: PlatformImage?
86+
var placeholderView: PlatformView? {
87+
didSet {
88+
oldValue?.removeFromSuperview()
89+
}
90+
}
8691
}
8792

8893
/// 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 {
203208
return
204209
}
205210
self.imageLoading.isLoading = true
211+
if imageModel.webOptions.contains(.delayPlaceholder) {
212+
self.imageConfiguration.placeholderView?.isHidden = true
213+
} else {
214+
self.imageConfiguration.placeholderView?.isHidden = false
215+
}
206216
view.wrapped.sd_setImage(with: imageModel.url, placeholderImage: imageConfiguration.placeholder, options: imageModel.webOptions, context: imageModel.webContext, progress: { (receivedSize, expectedSize, _) in
207217
let progress: Double
208218
if (expectedSize > 0) {
@@ -230,8 +240,10 @@ public struct AnimatedImage : PlatformViewRepresentable {
230240
self.imageLoading.isLoading = false
231241
self.imageLoading.progress = 1
232242
if let image = image {
243+
self.imageConfiguration.placeholderView?.isHidden = true
233244
self.imageHandler.successBlock?(image, cacheType)
234245
} else {
246+
self.imageConfiguration.placeholderView?.isHidden = false
235247
self.imageHandler.failureBlock?(error ?? NSError())
236248
}
237249
}
@@ -263,6 +275,21 @@ public struct AnimatedImage : PlatformViewRepresentable {
263275
} else if let url = imageModel.url, url != view.wrapped.sd_imageURL {
264276
view.wrapped.sd_imageIndicator = imageConfiguration.indicator
265277
view.wrapped.sd_imageTransition = imageConfiguration.transition
278+
if let placeholderView = imageConfiguration.placeholderView {
279+
placeholderView.removeFromSuperview()
280+
placeholderView.isHidden = true
281+
// Placeholder View should below the Indicator View
282+
if let indicatorView = imageConfiguration.indicator?.indicatorView {
283+
#if os(macOS)
284+
view.wrapped.addSubview(placeholderView, positioned: .below, relativeTo: indicatorView)
285+
#else
286+
view.wrapped.insertSubview(placeholderView, belowSubview: indicatorView)
287+
#endif
288+
} else {
289+
view.wrapped.addSubview(placeholderView)
290+
}
291+
placeholderView.bindFrameToSuperviewBounds()
292+
}
266293
loadImage(view, context: context)
267294
}
268295

@@ -728,8 +755,21 @@ extension AnimatedImage {
728755

729756
/// Associate a placeholder when loading image with url
730757
/// - Parameter content: A view that describes the placeholder.
731-
public func placeholder(_ placeholder: PlatformImage?) -> AnimatedImage {
732-
self.imageConfiguration.placeholder = placeholder
758+
/// - 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
759+
public func placeholder<T>(@ViewBuilder content: () -> T) -> AnimatedImage where T : View {
760+
#if os(macOS)
761+
let hostingView = NSHostingView(rootView: content())
762+
#else
763+
let hostingView = _UIHostingView(rootView: content())
764+
#endif
765+
self.imageConfiguration.placeholderView = hostingView
766+
return self
767+
}
768+
769+
/// Associate a placeholder image when loading image with url
770+
/// - Parameter content: A view that describes the placeholder.
771+
public func placeholder(_ image: PlatformImage?) -> AnimatedImage {
772+
self.imageConfiguration.placeholder = image
733773
return self
734774
}
735775

SDWebImageSwiftUI/Classes/ImageManager.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ public final class ImageManager : ObservableObject {
123123
manager.loadImage(with: url, options: options, context: context, progress: nil) { (image, data, error, cacheType, finished, imageUrl) in
124124
// This will callback immediately
125125
self.image = image
126+
if let image = image {
127+
self.successBlock?(image, cacheType)
128+
}
126129
}
127130
}
128131

SDWebImageSwiftUI/Classes/ImageViewWrapper.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,21 @@ public class ProgressIndicatorWrapper : PlatformView {
122122
addSubview(wrapped)
123123
}
124124
}
125+
extension PlatformView {
126+
/// Adds constraints to this `UIView` instances `superview` object to make sure this always has the same size as the superview.
127+
/// Please note that this has no effect if its `superview` is `nil` – add this `UIView` instance as a subview before calling this.
128+
func bindFrameToSuperviewBounds() {
129+
guard let superview = self.superview else {
130+
print("Error! `superview` was nil – call `addSubview(view: UIView)` before calling `bindFrameToSuperviewBounds()` to fix this.")
131+
return
132+
}
133+
134+
self.translatesAutoresizingMaskIntoConstraints = false
135+
self.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0).isActive = true
136+
self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0).isActive = true
137+
self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0).isActive = true
138+
self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: 0).isActive = true
139+
}
140+
}
125141

126142
#endif

Tests/AnimatedImageTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ class AnimatedImageTests: XCTestCase {
163163
XCTAssertEqual(context.coordinator.userInfo?["foo"] as? String, "bar")
164164
}
165165
.placeholder(PlatformImage())
166+
.placeholder {
167+
Circle()
168+
}
166169
.indicator(SDWebImageActivityIndicator.medium)
167170
// Image
168171
.resizable()

Tests/WebImageTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class WebImageTests: XCTestCase {
8282
.onProgress { _, _ in
8383

8484
}
85+
.placeholder(.init(platformImage: PlatformImage()))
8586
.placeholder {
8687
Circle()
8788
}

0 commit comments

Comments
 (0)