Skip to content

Commit eb0eafa

Browse files
authored
Merge pull request #275 from SDWebImage/api/asyncimage
Update the WebImage API to match SwiftUI.AsyncImage
2 parents 5c6bfdf + e96aaa9 commit eb0eafa

File tree

7 files changed

+164
-194
lines changed

7 files changed

+164
-194
lines changed

Diff for: Example/SDWebImageSwiftUIDemo/ContentView.swift

+13-20
Original file line numberDiff line numberDiff line change
@@ -96,36 +96,28 @@ struct ContentView: View {
9696
HStack {
9797
if self.animated {
9898
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS)
99-
AnimatedImage(url: URL(string:url), isAnimating: .constant(true))
99+
AnimatedImage(url: URL(string:url))
100100
.onViewUpdate { view, context in
101101
#if os(macOS)
102102
view.toolTip = url
103103
#endif
104104
}
105-
.indicator(SDWebImageActivityIndicator.medium)
106-
/**
107-
.placeholder(UIImage(systemName: "photo"))
108-
*/
105+
.indicator(.activity)
109106
.transition(.fade)
110107
.resizable()
111108
.scaledToFit()
112109
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
113110
#else
114-
WebImage(url: URL(string:url), isAnimating: self.$animated)
111+
WebImage(url: URL(string:url))
115112
.resizable()
116113
.indicator(.activity)
117114
.transition(.fade(duration: 0.5))
118115
.scaledToFit()
119116
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
120117
#endif
121118
} else {
122-
WebImage(url: URL(string:url), isAnimating: .constant(true))
119+
WebImage(url: URL(string:url))
123120
.resizable()
124-
/**
125-
.placeholder {
126-
Image(systemName: "photo")
127-
}
128-
*/
129121
.indicator(.activity)
130122
.transition(.fade(duration: 0.5))
131123
.scaledToFit()
@@ -199,15 +191,16 @@ struct ContentView: View {
199191
}
200192
#endif
201193
#if os(watchOS)
202-
return contentView()
203-
.contextMenu {
204-
Button(action: { self.reloadCache() }) {
205-
Text("Reload")
206-
}
207-
Button(action: { self.switchView() }) {
208-
Text("Switch")
194+
return NavigationView {
195+
contentView()
196+
.navigationTitle("WebImage")
197+
.toolbar {
198+
Button(action: { self.reloadCache() }) {
199+
Text("Reload")
200+
}
209201
}
210-
}
202+
203+
}
211204
#endif
212205
}
213206

Diff for: Example/SDWebImageSwiftUIDemo/DetailView.swift

+20-15
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,8 @@ struct DetailView: View {
5252
#endif
5353
#if os(macOS) || os(watchOS)
5454
zoomView()
55-
.contextMenu {
56-
Button(isAnimating ? "Stop" : "Start") {
57-
self.isAnimating.toggle()
58-
}
55+
.onTapGesture {
56+
self.isAnimating.toggle()
5957
}
6058
#endif
6159
}
@@ -95,24 +93,31 @@ struct DetailView: View {
9593
HStack {
9694
if animated {
9795
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS)
98-
AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating)
96+
AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating, placeholderImage: .wifiExclamationmark)
97+
.indicator(.progress)
9998
.resizable()
100-
.placeholder(.wifiExclamationmark)
101-
.indicator(SDWebImageProgressIndicator.default)
10299
.scaledToFit()
103100
#else
104-
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating)
105-
.resizable()
106-
.placeholder(.wifiExclamationmark)
101+
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
102+
image.resizable()
103+
.scaledToFit()
104+
} placeholder: {
105+
Image.wifiExclamationmark
106+
.resizable()
107+
.scaledToFit()
108+
}
107109
.indicator(.progress)
108-
.scaledToFit()
109110
#endif
110111
} else {
111-
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating)
112-
.resizable()
113-
.placeholder(.wifiExclamationmark)
112+
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
113+
image.resizable()
114+
.scaledToFit()
115+
} placeholder: {
116+
Image.wifiExclamationmark
117+
.resizable()
118+
.scaledToFit()
119+
}
114120
.indicator(.progress(style: .circular))
115-
.scaledToFit()
116121
}
117122
}
118123
}

Diff for: README.md

+14-16
Original file line numberDiff line numberDiff line change
@@ -128,18 +128,16 @@ github "SDWebImage/SDWebImageSwiftUI"
128128

129129
```swift
130130
var body: some View {
131-
WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic"))
131+
WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic")) { image in
132+
image.resizable() // Control layout like SwiftUI.AsyncImage, you must use this modifier or the view will use the image bitmap size
133+
} placeholder: {
134+
Rectangle().foregroundColor(.gray)
135+
}
132136
// Supports options and context, like `.delayPlaceholder` to show placeholder only when error
133137
.onSuccess { image, data, cacheType in
134138
// Success
135139
// Note: Data exist only when queried from disk cache or network. Use `.queryMemoryData` if you really need data
136140
}
137-
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
138-
.placeholder(Image(systemName: "photo")) // Placeholder Image
139-
// Supports ViewBuilder as well
140-
.placeholder {
141-
Rectangle().foregroundColor(.gray)
142-
}
143141
.indicator(.activity) // Activity Indicator
144142
.transition(.fade(duration: 0.5)) // Fade Transition with duration
145143
.scaledToFit()
@@ -194,21 +192,21 @@ WebImage(url: url)
194192
```swift
195193
var body: some View {
196194
Group {
197-
AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"))
195+
AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"), placeholderImage: .init(systemName: "photo")) // Placeholder Image
198196
// Supports options and context, like `.progressiveLoad` for progressive animation loading
199197
.onFailure { error in
200198
// Error
201199
}
202200
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
203-
.placeholder(UIImage(systemName: "photo")) // Placeholder Image
204-
// Supports ViewBuilder as well
205-
.placeholder {
206-
Circle().foregroundColor(.gray)
207-
}
208-
.indicator(SDWebImageActivityIndicator.medium) // Activity Indicator
201+
.indicator(.activity) // Activity Indicator
209202
.transition(.fade) // Fade Transition
210203
.scaledToFit() // Attention to call it on AnimatedImage, but not `some View` after View Modifier (Swift Protocol Extension method is static dispatched)
211204

205+
// Supports SwiftUI ViewBuilder placeholder as well
206+
AnimatedImage(url: url) {
207+
Circle().foregroundColor(.gray)
208+
}
209+
212210
// Data
213211
AnimatedImage(data: try! Data(contentsOf: URL(fileURLWithPath: "/tmp/foo.webp")))
214212
.customLoopCount(1) // Custom loop count
@@ -624,8 +622,8 @@ Since SwiftUI is aimed to support all Apple platforms, our demo does this as wel
624622

625623
Demo Tips:
626624

627-
1. Use `Switch` (right-click on macOS/force press on watchOS) to switch between `WebImage` and `AnimatedImage`.
628-
2. Use `Reload` (right-click on macOS/force press on watchOS) to clear cache.
625+
1. Use `Switch` (right-click on macOS/tap on watchOS) to switch between `WebImage` and `AnimatedImage`.
626+
2. Use `Reload` (right-click on macOS/button on watchOS) to clear cache.
629627
3. Use `Swipe Left` (menu button on tvOS) to delete one image url from list.
630628
4. Pinch gesture (Digital Crown on watchOS, play button on tvOS) to zoom-in detail page image.
631629
5. Clear cache and go to detail page to see progressive loading.

Diff for: SDWebImageSwiftUI/Classes/AnimatedImage.swift

+39-70
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ final class AnimatedImageModel : ObservableObject {
3131
@Published var url: URL?
3232
@Published var webOptions: SDWebImageOptions = []
3333
@Published var webContext: [SDWebImageContextOption : Any]? = nil
34+
@Published var placeholderImage: PlatformImage?
35+
@Published var placeholderView: PlatformView? {
36+
didSet {
37+
oldValue?.removeFromSuperview()
38+
}
39+
}
3440
/// Name image
3541
@Published var name: String?
3642
@Published var bundle: Bundle?
@@ -90,12 +96,6 @@ final class AnimatedImageConfiguration: ObservableObject {
9096
// These configurations only useful for web image loading
9197
var indicator: SDWebImageIndicator?
9298
var transition: SDWebImageTransition?
93-
var placeholder: PlatformImage?
94-
var placeholderView: PlatformView? {
95-
didSet {
96-
oldValue?.removeFromSuperview()
97-
}
98-
}
9999
}
100100

101101
/// A Image View type to load image from url, data or bundle. Supports animated and static image format.
@@ -115,13 +115,19 @@ public struct AnimatedImage : PlatformViewRepresentable {
115115
/// True to start animation, false to stop animation.
116116
@Binding public var isAnimating: Bool
117117

118-
/// Create an animated image with url, placeholder, custom options and context.
118+
/// Create an animated image with url, placeholder, custom options and context, including animation control binding.
119119
/// - Parameter url: The image url
120120
/// - Parameter placeholder: The placeholder image to show during loading
121121
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
122122
/// - 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.
123-
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
124-
self.init(url: url, options: options, context: context, isAnimating: .constant(true))
123+
/// - Parameter isAnimating: The binding for animation control
124+
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), placeholderImage: PlatformImage? = nil) {
125+
let imageModel = AnimatedImageModel()
126+
imageModel.url = url
127+
imageModel.webOptions = options
128+
imageModel.webContext = context
129+
imageModel.placeholderImage = placeholderImage
130+
self.init(imageModel: imageModel, isAnimating: isAnimating)
125131
}
126132

127133
/// Create an animated image with url, placeholder, custom options and context, including animation control binding.
@@ -130,46 +136,37 @@ public struct AnimatedImage : PlatformViewRepresentable {
130136
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
131137
/// - 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.
132138
/// - Parameter isAnimating: The binding for animation control
133-
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
139+
public init<T>(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), @ViewBuilder placeholder: @escaping () -> T) where T : View {
134140
let imageModel = AnimatedImageModel()
135141
imageModel.url = url
136142
imageModel.webOptions = options
137143
imageModel.webContext = context
144+
#if os(macOS)
145+
let hostingView = NSHostingView(rootView: placeholder())
146+
#else
147+
let hostingView = _UIHostingView(rootView: placeholder())
148+
#endif
149+
imageModel.placeholderView = hostingView
138150
self.init(imageModel: imageModel, isAnimating: isAnimating)
139151
}
140152

141-
/// Create an animated image with name and bundle.
142-
/// - Note: Asset Catalog is not supported.
143-
/// - Parameter name: The image name
144-
/// - Parameter bundle: The bundle contains image
145-
public init(name: String, bundle: Bundle? = nil) {
146-
self.init(name: name, bundle: bundle, isAnimating: .constant(true))
147-
}
148-
149153
/// Create an animated image with name and bundle, including animation control binding.
150154
/// - Note: Asset Catalog is not supported.
151155
/// - Parameter name: The image name
152156
/// - Parameter bundle: The bundle contains image
153157
/// - Parameter isAnimating: The binding for animation control
154-
public init(name: String, bundle: Bundle? = nil, isAnimating: Binding<Bool>) {
158+
public init(name: String, bundle: Bundle? = nil, isAnimating: Binding<Bool> = .constant(true)) {
155159
let imageModel = AnimatedImageModel()
156160
imageModel.name = name
157161
imageModel.bundle = bundle
158162
self.init(imageModel: imageModel, isAnimating: isAnimating)
159163
}
160164

161-
/// Create an animated image with data and scale.
162-
/// - Parameter data: The image data
163-
/// - Parameter scale: The scale factor
164-
public init(data: Data, scale: CGFloat = 1) {
165-
self.init(data: data, scale: scale, isAnimating: .constant(true))
166-
}
167-
168165
/// Create an animated image with data and scale, including animation control binding.
169166
/// - Parameter data: The image data
170167
/// - Parameter scale: The scale factor
171168
/// - Parameter isAnimating: The binding for animation control
172-
public init(data: Data, scale: CGFloat = 1, isAnimating: Binding<Bool>) {
169+
public init(data: Data, scale: CGFloat = 1, isAnimating: Binding<Bool> = .constant(true)) {
173170
let imageModel = AnimatedImageModel()
174171
imageModel.data = data
175172
imageModel.scale = scale
@@ -222,7 +219,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
222219
func setupIndicator(_ view: AnimatedImageViewWrapper, context: Context) {
223220
view.wrapped.sd_imageIndicator = imageConfiguration.indicator
224221
view.wrapped.sd_imageTransition = imageConfiguration.transition
225-
if let placeholderView = imageConfiguration.placeholderView {
222+
if let placeholderView = imageModel.placeholderView {
226223
placeholderView.removeFromSuperview()
227224
placeholderView.isHidden = true
228225
// Placeholder View should below the Indicator View
@@ -243,13 +240,13 @@ public struct AnimatedImage : PlatformViewRepresentable {
243240
context.coordinator.imageLoading.isLoading = true
244241
let webOptions = imageModel.webOptions
245242
if webOptions.contains(.delayPlaceholder) {
246-
self.imageConfiguration.placeholderView?.isHidden = true
243+
self.imageModel.placeholderView?.isHidden = true
247244
} else {
248-
self.imageConfiguration.placeholderView?.isHidden = false
245+
self.imageModel.placeholderView?.isHidden = false
249246
}
250247
var webContext = imageModel.webContext ?? [:]
251248
webContext[.animatedImageClass] = SDAnimatedImage.self
252-
view.wrapped.sd_internalSetImage(with: imageModel.url, placeholderImage: imageConfiguration.placeholder, options: webOptions, context: webContext, setImageBlock: nil, progress: { (receivedSize, expectedSize, _) in
249+
view.wrapped.sd_internalSetImage(with: imageModel.url, placeholderImage: imageModel.placeholderImage, options: webOptions, context: webContext, setImageBlock: nil, progress: { (receivedSize, expectedSize, _) in
253250
let progress: Double
254251
if (expectedSize > 0) {
255252
progress = Double(receivedSize) / Double(expectedSize)
@@ -265,10 +262,10 @@ public struct AnimatedImage : PlatformViewRepresentable {
265262
context.coordinator.imageLoading.isLoading = false
266263
context.coordinator.imageLoading.progress = 1
267264
if let image = image {
268-
self.imageConfiguration.placeholderView?.isHidden = true
265+
self.imageModel.placeholderView?.isHidden = true
269266
self.imageHandler.successBlock?(image, data, cacheType)
270267
} else {
271-
self.imageConfiguration.placeholderView?.isHidden = false
268+
self.imageModel.placeholderView?.isHidden = false
272269
self.imageHandler.failureBlock?(error ?? NSError())
273270
}
274271
}
@@ -794,30 +791,19 @@ extension AnimatedImage {
794791
}
795792
}
796793

794+
// Convenient indicator dot syntax
795+
extension SDWebImageIndicator where Self == SDWebImageActivityIndicator {
796+
public static var activity: Self { Self() }
797+
}
798+
799+
extension SDWebImageIndicator where Self == SDWebImageProgressIndicator {
800+
public static var progress: Self { Self() }
801+
}
802+
797803
// Web Image convenience, based on UIKit/AppKit API
798804
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
799805
extension AnimatedImage {
800806

801-
/// Associate a placeholder when loading image with url
802-
/// - Parameter content: A view that describes the placeholder.
803-
/// - 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
804-
public func placeholder<T>(@ViewBuilder content: () -> T) -> AnimatedImage where T : View {
805-
#if os(macOS)
806-
let hostingView = NSHostingView(rootView: content())
807-
#else
808-
let hostingView = _UIHostingView(rootView: content())
809-
#endif
810-
self.imageConfiguration.placeholderView = hostingView
811-
return self
812-
}
813-
814-
/// Associate a placeholder image when loading image with url
815-
/// - Parameter content: A view that describes the placeholder.
816-
public func placeholder(_ image: PlatformImage?) -> AnimatedImage {
817-
self.imageConfiguration.placeholder = image
818-
return self
819-
}
820-
821807
/// Associate a indicator when loading image with url
822808
/// - Note: If you do not need indicator, specify nil. Defaults to nil
823809
/// - Parameter indicator: indicator, see more in `SDWebImageIndicator`
@@ -835,23 +821,6 @@ extension AnimatedImage {
835821
}
836822
}
837823

838-
// Indicator
839-
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
840-
extension AnimatedImage {
841-
842-
/// Associate a indicator when loading image with url
843-
/// - Parameter indicator: The indicator type, see `Indicator`
844-
public func indicator<T>(_ indicator: Indicator<T>) -> some View where T : View {
845-
return self.modifier(IndicatorViewModifier(status: indicatorStatus, indicator: indicator))
846-
}
847-
848-
/// Associate a indicator when loading image with url, convenient method with block
849-
/// - Parameter content: A view that describes the indicator.
850-
public func indicator<T>(@ViewBuilder content: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<Double>) -> T) -> some View where T : View {
851-
return indicator(Indicator(content: content))
852-
}
853-
}
854-
855824
#if DEBUG
856825
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
857826
struct AnimatedImage_Previews : PreviewProvider {

0 commit comments

Comments
 (0)