Skip to content

Commit bbf6d59

Browse files
authored
Merge pull request #40 from SDWebImage/refactory_placeholder
Refactory placeholder to support custom View Builder
2 parents aebd140 + 9dcc697 commit bbf6d59

File tree

5 files changed

+107
-55
lines changed

5 files changed

+107
-55
lines changed

Example/SDWebImageSwiftUIDemo/ContentView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ struct ContentView: View {
8686
#if os(macOS) || os(iOS) || os(tvOS)
8787
AnimatedImage(url: URL(string:url))
8888
.indicator(SDWebImageActivityIndicator.medium)
89+
/**
90+
.placeholder(UIImage(systemName: "photo"))
91+
*/
8992
.transition(.fade)
9093
.resizable()
9194
.scaledToFit()
@@ -100,6 +103,11 @@ struct ContentView: View {
100103
#if os(macOS) || os(iOS) || os(tvOS)
101104
WebImage(url: URL(string:url))
102105
.resizable()
106+
/**
107+
.placeholder {
108+
Image(systemName: "photo")
109+
}
110+
*/
103111
.indicator(.activity)
104112
.animation(.easeInOut(duration: 0.5))
105113
.transition(.fade)

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,11 @@ var body: some View {
9090
// Success
9191
}
9292
.resizable() // Resizable like SwiftUI.Image
93+
.placeholder {
94+
Image(systemName: "photo") // Placeholder
95+
}
9396
.indicator(.activity) // Activity Indicator
94-
.animation(.easeInOut(duration: 0.5))
97+
.animation(.easeInOut(duration: 0.5)) // Animation Duration
9598
.transition(.fade) // Fade Transition
9699
.scaledToFit()
97100
.frame(width: 300, height: 300, alignment: .center)
@@ -118,6 +121,7 @@ var body: some View {
118121
// Error
119122
}
120123
.resizable() // Actually this is not needed unlike SwiftUI.Image
124+
.placeholder(UIImage(systemName: "photo")) // Placeholder
121125
.indicator(SDWebImageActivityIndicator.medium) // Activity Indicator
122126
.transition(.fade) // Fade Transition
123127
.scaledToFit() // Attention to call it on AnimatedImage, but not `some View` after View Modifier

SDWebImageSwiftUI/Classes/AnimatedImage.swift

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ final class AnimatedImageConfiguration: ObservableObject {
4747
@Published var indicator: SDWebImageIndicator?
4848
@Published var transition: SDWebImageTransition?
4949
#endif
50+
@Published var placeholder: PlatformImage?
5051
}
5152

5253
// Convenient
@@ -67,7 +68,6 @@ public struct AnimatedImage : PlatformViewRepresentable {
6768
@ObservedObject var imageCoordinator = AnimatedImageCoordinator()
6869

6970
var url: URL?
70-
var placeholder: PlatformImage?
7171
var webOptions: SDWebImageOptions = []
7272
var webContext: [SDWebImageContextOption : Any]? = nil
7373

@@ -80,8 +80,8 @@ public struct AnimatedImage : PlatformViewRepresentable {
8080
/// - Parameter placeholder: The placeholder image to show during loading
8181
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
8282
/// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
83-
public init(url: URL?, placeholder: PlatformImage? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
84-
self.init(url: url, placeholder: placeholder, options: options, context: context, isAnimating: .constant(true))
83+
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
84+
self.init(url: url, options: options, context: context, isAnimating: .constant(true))
8585
}
8686

8787
/// Create an animated image with url, placeholder, custom options and context, including animation control binding.
@@ -90,9 +90,8 @@ public struct AnimatedImage : PlatformViewRepresentable {
9090
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
9191
/// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
9292
/// - Parameter isAnimating: The binding for animation control
93-
public init(url: URL?, placeholder: PlatformImage? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
93+
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
9494
self._isAnimating = isAnimating
95-
self.placeholder = placeholder
9695
self.webOptions = options
9796
self.webContext = context
9897
self.url = url
@@ -190,7 +189,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
190189
if currentOperation != nil {
191190
return
192191
}
193-
view.wrapped.sd_setImage(with: url, placeholderImage: placeholder, options: webOptions, context: webContext, progress: { (receivedSize, expectedSize, _) in
192+
view.wrapped.sd_setImage(with: url, placeholderImage: imageConfiguration.placeholder, options: webOptions, context: webContext, progress: { (receivedSize, expectedSize, _) in
194193
self.imageModel.progressBlock?(receivedSize, expectedSize)
195194
}) { (image, error, cacheType, _) in
196195
if let image = image {
@@ -522,12 +521,14 @@ extension AnimatedImage {
522521
#if os(macOS) || os(iOS) || os(tvOS)
523522
return self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)
524523
#else
525-
if let aspectRatio = aspectRatio {
526-
return AnyView(self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode))
527-
} else {
528-
// on watchOS, there are no workaround like `AnimatedImageViewWrapper` to override `intrinsicContentSize`, so the aspect ratio is undetermined and cause sizing issues
529-
// To workaround, we do not call default implementation for this case, using original solution instead
530-
return AnyView(self)
524+
return Group {
525+
if aspectRatio != nil {
526+
self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)
527+
} else {
528+
// on watchOS, there are no workaround like `AnimatedImageViewWrapper` to override `intrinsicContentSize`, so the aspect ratio is undetermined and cause sizing issues
529+
// To workaround, we do not call default implementation for this case, using original solution instead
530+
self
531+
}
531532
}
532533
#endif
533534
}
@@ -650,10 +651,17 @@ extension AnimatedImage {
650651
}
651652
}
652653

653-
#if os(macOS) || os(iOS) || os(tvOS)
654654
// Web Image convenience
655655
extension AnimatedImage {
656656

657+
/// Associate a placeholder when loading image with url
658+
/// - Parameter content: A view that describes the placeholder.
659+
public func placeholder(_ placeholder: PlatformImage?) -> AnimatedImage {
660+
imageConfiguration.placeholder = placeholder
661+
return self
662+
}
663+
664+
#if os(macOS) || os(iOS) || os(tvOS)
657665
/// Associate a indicator when loading image with url
658666
/// - Note: If you do not need indicator, specify nil. Defaults to nil
659667
/// - Parameter indicator: indicator, see more in `SDWebImageIndicator`
@@ -669,8 +677,8 @@ extension AnimatedImage {
669677
imageConfiguration.transition = transition
670678
return self
671679
}
680+
#endif
672681
}
673-
#endif
674682

675683
#if DEBUG
676684
struct AnimatedImage_Previews : PreviewProvider {

SDWebImageSwiftUI/Classes/Indicator/Indicator.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import SwiftUI
1111

1212
/// A type to build the indicator
1313
public struct Indicator<T> where T : View {
14-
var builder: (Binding<Bool>, Binding<CGFloat>) -> T
14+
var content: (Binding<Bool>, Binding<CGFloat>) -> T
1515

1616
/// Create a indicator with builder
1717
/// - Parameter builder: A builder to build indicator
1818
/// - Parameter isAnimating: A Binding to control the animation. If image is during loading, the value is true, else (like start loading) the value is false.
1919
/// - Parameter progress: A Binding to control the progress during loading. If no progress can be reported, the value is 0.
2020
/// Associate a indicator when loading image with url
21-
public init(@ViewBuilder builder: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<CGFloat>) -> T) {
22-
self.builder = builder
21+
public init(@ViewBuilder content: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<CGFloat>) -> T) {
22+
self.content = content
2323
}
2424
}
2525

@@ -32,17 +32,17 @@ struct IndicatorViewModifier<T> : ViewModifier where T : View {
3232
var indicator: Indicator<T>
3333

3434
func body(content: Content) -> some View {
35-
if imageManager.isFinished {
36-
// Disable Indiactor
37-
return AnyView(content)
38-
} else {
39-
// Enable indicator
40-
return AnyView(
35+
Group {
36+
if imageManager.isFinished {
37+
// Disable Indiactor
38+
content
39+
} else {
40+
// Enable indicator
4141
ZStack {
4242
content
43-
indicator.builder($imageManager.isLoading, $imageManager.progress)
43+
indicator.content($imageManager.isLoading, $imageManager.progress)
4444
}
45-
)
45+
}
4646
}
4747
}
4848
}

SDWebImageSwiftUI/Classes/WebImage.swift

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,23 @@ public struct WebImage : View {
1313
static var emptyImage = PlatformImage()
1414

1515
var url: URL?
16-
var placeholder: Image?
1716
var options: SDWebImageOptions
1817
var context: [SDWebImageContextOption : Any]?
1918

2019
var configurations: [(Image) -> Image] = []
2120

21+
var placeholder: AnyView?
22+
var retryOnAppear: Bool = true
23+
var cancelOnDisappear: Bool = true
24+
2225
@ObservedObject var imageManager: ImageManager
2326

2427
/// Create a web image with url, placeholder, custom options and context.
2528
/// - Parameter url: The image url
26-
/// - Parameter placeholder: The placeholder image to show during loading
2729
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
2830
/// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
29-
public init(url: URL?, placeholder: Image? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
31+
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
3032
self.url = url
31-
self.placeholder = placeholder
3233
self.options = options
3334
self.context = context
3435
self.imageManager = ImageManager(url: url, options: options, context: context)
@@ -38,31 +39,33 @@ public struct WebImage : View {
3839
}
3940

4041
public var body: some View {
41-
if let platformImage = imageManager.image {
42-
var image = Image(platformImage: platformImage)
43-
image = configurations.reduce(image) { (previous, configuration) in
44-
configuration(previous)
45-
}
46-
let view = image
47-
return AnyView(view)
48-
} else {
49-
var image = placeholder ?? Image(platformImage: WebImage.emptyImage)
50-
image = configurations.reduce(image) { (previous, configuration) in
51-
configuration(previous)
52-
}
53-
let view = image
54-
.onAppear {
55-
if !self.imageManager.isFinished {
56-
self.imageManager.load()
42+
Group {
43+
if imageManager.image != nil {
44+
configurations.reduce(Image(platformImage: imageManager.image!)) { (previous, configuration) in
45+
configuration(previous)
5746
}
58-
}
59-
.onDisappear {
60-
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
61-
if self.imageManager.isLoading && !self.imageManager.isIncremental {
62-
self.imageManager.cancel()
47+
} else {
48+
Group {
49+
if placeholder != nil {
50+
placeholder
51+
} else {
52+
Image(platformImage: WebImage.emptyImage)
53+
}
54+
}
55+
.onAppear {
56+
guard self.retryOnAppear else { return }
57+
if !self.imageManager.isFinished {
58+
self.imageManager.load()
59+
}
60+
}
61+
.onDisappear {
62+
guard self.cancelOnDisappear else { return }
63+
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
64+
if self.imageManager.isLoading && !self.imageManager.isIncremental {
65+
self.imageManager.cancel()
66+
}
6367
}
6468
}
65-
return AnyView(view)
6669
}
6770
}
6871
}
@@ -135,6 +138,35 @@ extension WebImage {
135138
}
136139
}
137140

141+
// WebImage Modifier
142+
extension WebImage {
143+
144+
/// Associate a placeholder when loading image with url
145+
/// - note: The differences between Placeholder and Indicator, is that placeholder does not supports animation, and return type is different
146+
/// - Parameter content: A view that describes the placeholder.
147+
public func placeholder<T>(@ViewBuilder _ content: () -> T) -> WebImage where T : View {
148+
var result = self
149+
result.placeholder = AnyView(content())
150+
return result
151+
}
152+
153+
/// Control the behavior to retry the failed loading when view become appears again
154+
/// - Parameter flag: Whether or not to retry the failed loading
155+
public func retryOnAppear(_ flag: Bool) -> WebImage {
156+
var result = self
157+
result.retryOnAppear = flag
158+
return result
159+
}
160+
161+
/// Control the behavior to cancel the pending loading when view become disappear again
162+
/// - Parameter flag: Whether or not to cancel the pending loading
163+
public func cancelOnDisappear(_ flag: Bool) -> WebImage {
164+
var result = self
165+
result.cancelOnDisappear = flag
166+
return result
167+
}
168+
}
169+
138170
// Indicator
139171
extension WebImage {
140172

@@ -145,9 +177,9 @@ extension WebImage {
145177
}
146178

147179
/// Associate a indicator when loading image with url, convenient method with block
148-
/// - Parameter indicator: The indicator type, see `Indicator`
149-
public func indicator<T>(@ViewBuilder builder: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<CGFloat>) -> T) -> some View where T : View {
150-
return indicator(Indicator(builder: builder))
180+
/// - Parameter content: A view that describes the indicator.
181+
public func indicator<T>(@ViewBuilder content: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<CGFloat>) -> T) -> some View where T : View {
182+
return indicator(Indicator(content: content))
151183
}
152184
}
153185

0 commit comments

Comments
 (0)