Skip to content

Commit 9ec9e29

Browse files
committed
Update the WebImage API to match SwiftUI.AsyncImage (not SwiftUI.Image), make it more easy to replace
The old API is still kept, except the .placeholder one
1 parent 86b1901 commit 9ec9e29

File tree

2 files changed

+85
-70
lines changed

2 files changed

+85
-70
lines changed

Diff for: Example/SDWebImageSwiftUIDemo/DetailView.swift

+16-8
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,26 @@ struct DetailView: View {
101101
.indicator(SDWebImageProgressIndicator.default)
102102
.scaledToFit()
103103
#else
104-
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating)
105-
.resizable()
106-
.placeholder(.wifiExclamationmark)
104+
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
105+
image.resizable()
106+
.scaledToFit()
107+
} placeholder: {
108+
Image.wifiExclamationmark
109+
.resizable()
110+
.scaledToFit()
111+
}
107112
.indicator(.progress)
108-
.scaledToFit()
109113
#endif
110114
} else {
111-
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating)
112-
.resizable()
113-
.placeholder(.wifiExclamationmark)
115+
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
116+
image.resizable()
117+
.scaledToFit()
118+
} placeholder: {
119+
Image.wifiExclamationmark
120+
.resizable()
121+
.scaledToFit()
122+
}
114123
.indicator(.progress(style: .circular))
115-
.scaledToFit()
116124
}
117125
}
118126
}

Diff for: SDWebImageSwiftUI/Classes/WebImage.swift

+69-62
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,43 @@
99
import SwiftUI
1010
import SDWebImage
1111

12+
public enum WebImagePhase {
13+
/// No image is loaded.
14+
case empty
15+
16+
/// An image succesfully loaded.
17+
case success(Image)
18+
19+
/// An image failed to load with an error.
20+
case failure(Error)
21+
22+
/// The loaded image, if any.
23+
///
24+
/// If this value isn't `nil`, the image load operation has finished,
25+
/// and you can use the image to update the view. You can use the image
26+
/// directly, or you can modify it in some way. For example, you can add
27+
/// a ``Image/resizable(capInsets:resizingMode:)`` modifier to make the
28+
/// image resizable.
29+
public var image: Image? {
30+
switch self {
31+
case let .success(image):
32+
return image
33+
case .empty, .failure:
34+
return nil
35+
}
36+
}
37+
38+
/// The error that occurred when attempting to load an image, if any.
39+
public var error: Error? {
40+
switch self {
41+
case .empty, .success:
42+
return nil
43+
case let .failure(error):
44+
return error
45+
}
46+
}
47+
}
48+
1249
/// Data Binding Object, only properties in this object can support changes from user with @State and refresh
1350
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
1451
final class WebImageModel : ObservableObject {
@@ -43,10 +80,12 @@ final class WebImageConfiguration: ObservableObject {
4380

4481
/// A Image View type to load image from url. Supports static/animated image format.
4582
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
46-
public struct WebImage : View {
83+
public struct WebImage<Content> : View where Content: View {
84+
var transaction: Transaction
85+
4786
var configurations: [(Image) -> Image] = []
4887

49-
var placeholder: AnyView?
88+
var content: (WebImagePhase) -> Content
5089

5190
/// A Binding to control the animation. You can bind external logic to control the animation status.
5291
/// True to start animation, false to stop animation.
@@ -72,7 +111,23 @@ public struct WebImage : View {
72111
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
73112
/// - 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.
74113
/// - Parameter isAnimating: The binding for animation control. The binding value should be `true` when initialized to setup the correct animated image class. If not, you must provide the `.animatedImageClass` explicitly. When the animation started, this binding can been used to start / stop the animation.
75-
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
114+
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true)) where Content == Image {
115+
self.init(url: url, options: options, context: context, isAnimating: isAnimating) { phase in
116+
phase.image ?? Image(platformImage: .empty)
117+
}
118+
}
119+
120+
public init<I, P>(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I: View, P: View {
121+
self.init(url: url, options: options, context: context, isAnimating: isAnimating) { phase in
122+
if let i = phase.image {
123+
content(i)
124+
} else {
125+
placeholder()
126+
}
127+
}
128+
}
129+
130+
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (WebImagePhase) -> Content) {
76131
self._isAnimating = isAnimating
77132
var context = context ?? [:]
78133
// provide animated image class if the initialized `isAnimating` is true, user can still custom the image class if they want
@@ -89,27 +144,16 @@ public struct WebImage : View {
89144
let imageManager = ImageManager()
90145
_imageManager = StateObject(wrappedValue: imageManager)
91146
_indicatorStatus = ObservedObject(wrappedValue: imageManager.indicatorStatus)
92-
}
93-
94-
/// Create a web image with url, placeholder, custom options and context.
95-
/// - Parameter url: The image url
96-
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
97-
/// - 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.
98-
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
99-
self.init(url: url, options: options, context: context, isAnimating: .constant(true))
147+
148+
self.transaction = transaction
149+
self.content = { phase in
150+
content(phase)
151+
}
100152
}
101153

102154
public var body: some View {
103155
// Container
104156
return ZStack {
105-
// This empty Image is used to receive container's level appear/disappear to start/stop player, reduce CPU usage
106-
Image(platformImage: .empty)
107-
.onAppear {
108-
self.appearAction()
109-
}
110-
.onDisappear {
111-
self.disappearAction()
112-
}
113157
// Render Logic for actual animated image frame or static image
114158
if imageManager.image != nil && imageModel.url == imageManager.currentURL {
115159
if isAnimating && !imageManager.isIncremental {
@@ -118,8 +162,8 @@ public struct WebImage : View {
118162
displayImage()
119163
}
120164
} else {
165+
content((imageManager.error != nil) ? .failure(imageManager.error!) : .empty)
121166
// Load Logic
122-
setupPlaceholder()
123167
.onPlatformAppear(appear: {
124168
self.setupManager()
125169
if (self.imageManager.error == nil) {
@@ -145,7 +189,7 @@ public struct WebImage : View {
145189
/// Configure the platform image into the SwiftUI rendering image
146190
func configure(image: PlatformImage) -> some View {
147191
// Actual rendering SwiftUI image
148-
let result: Image
192+
var result: Image
149193
// NSImage works well with SwiftUI, include Vector and EXIF images.
150194
#if os(macOS)
151195
result = Image(nsImage: image)
@@ -188,9 +232,12 @@ public struct WebImage : View {
188232

189233
// Should not use `EmptyView`, which does not respect to the container's frame modifier
190234
// Using a empty image instead for better compatible
191-
return configurations.reduce(result) { (previous, configuration) in
235+
let i = configurations.reduce(result) { (previous, configuration) in
192236
configuration(previous)
193237
}
238+
239+
// Apply view builder
240+
return content(.success(i))
194241
}
195242

196243
/// Image Manager status
@@ -279,25 +326,6 @@ public struct WebImage : View {
279326
}
280327
}
281328
}
282-
283-
/// Placeholder View Support
284-
func setupPlaceholder() -> some View {
285-
// Don't use `Group` because it will trigger `.onAppear` and `.onDisappear` when condition view removed, treat placeholder as an entire component
286-
let result: AnyView
287-
if let placeholder = placeholder {
288-
// If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :)
289-
if imageModel.options.contains(.delayPlaceholder) && imageManager.error == nil {
290-
result = AnyView(configure(image: .empty))
291-
} else {
292-
result = placeholder
293-
}
294-
} else {
295-
result = AnyView(configure(image: .empty))
296-
}
297-
// Custom ID to avoid SwiftUI engine cache the status, and does not call `onAppear` when placeholder not changed (See `ContentView.swift/ContentView2` case)
298-
// Because we load the image url in placeholder's `onAppear`, it should be called to sync with state changes :)
299-
return result.id(imageModel.url)
300-
}
301329
}
302330

303331
// Layout
@@ -373,27 +401,6 @@ extension WebImage {
373401
// WebImage Modifier
374402
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
375403
extension WebImage {
376-
377-
/// Associate a placeholder when loading image with url
378-
/// - note: The differences between Placeholder and Indicator, is that placeholder does not supports animation, and return type is different
379-
/// - Parameter content: A view that describes the placeholder.
380-
public func placeholder<T>(@ViewBuilder content: () -> T) -> WebImage where T : View {
381-
var result = self
382-
result.placeholder = AnyView(content())
383-
return result
384-
}
385-
386-
/// Associate a placeholder image when loading image with url
387-
/// - note: This placeholder image will apply the same size and resizable from WebImage for convenience. If you don't want this, use the ViewBuilder one above instead
388-
/// - Parameter image: A Image view that describes the placeholder.
389-
public func placeholder(_ image: Image) -> WebImage {
390-
return placeholder {
391-
configurations.reduce(image) { (previous, configuration) in
392-
configuration(previous)
393-
}
394-
}
395-
}
396-
397404
/// Control the behavior to retry the failed loading when view become appears again
398405
/// - Parameter flag: Whether or not to retry the failed loading
399406
public func retryOnAppear(_ flag: Bool) -> WebImage {

0 commit comments

Comments
 (0)