Skip to content

Commit 9240689

Browse files
committed
Update the API of AnimatedImage as well
1. Change the placeholder into Web URL init method (placeholder not works for data/bundle init method) 2. Add convenient .progress/.activity syntax for AnimatedImage indicator
1 parent 9ec9e29 commit 9240689

File tree

6 files changed

+79
-89
lines changed

6 files changed

+79
-89
lines changed

Diff for: Example/SDWebImageSwiftUIDemo/ContentView.swift

+17-8
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ struct ContentView: View {
102102
view.toolTip = url
103103
#endif
104104
}
105-
.indicator(SDWebImageActivityIndicator.medium)
105+
.indicator(.activity)
106106
/**
107107
.placeholder(UIImage(systemName: "photo"))
108108
*/
@@ -111,24 +111,33 @@ struct ContentView: View {
111111
.scaledToFit()
112112
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
113113
#else
114-
WebImage(url: URL(string:url), isAnimating: self.$animated)
115-
.resizable()
114+
WebImage(url: URL(string:url), isAnimating: self.$animated, transaction: .init(animation: .easeInOut(duration: 1))) { image in
115+
if let i = image.image {
116+
i.resizable()
117+
.scaledToFit()
118+
} else {
119+
Color.red
120+
}
121+
}
116122
.indicator(.activity)
117123
.transition(.fade(duration: 0.5))
118-
.scaledToFit()
119124
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
120125
#endif
121126
} else {
122-
WebImage(url: URL(string:url), isAnimating: .constant(true))
123-
.resizable()
127+
WebImage(url: URL(string:url), isAnimating: .constant(true), transaction: .init(animation: .easeInOut(duration: 1))) { image in
128+
if let i = image.image {
129+
i.resizable()
130+
.scaledToFit()
131+
} else {
132+
Color.red
133+
}
134+
}
124135
/**
125136
.placeholder {
126137
Image(systemName: "photo")
127138
}
128139
*/
129140
.indicator(.activity)
130-
.transition(.fade(duration: 0.5))
131-
.scaledToFit()
132141
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
133142
}
134143
Text((url as NSString).lastPathComponent)

Diff for: Example/SDWebImageSwiftUIDemo/DetailView.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,9 @@ struct DetailView: View {
9595
HStack {
9696
if animated {
9797
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS)
98-
AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating)
98+
AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating, placeholderImage: .wifiExclamationmark)
9999
.resizable()
100-
.placeholder(.wifiExclamationmark)
101-
.indicator(SDWebImageProgressIndicator.default)
100+
.indicator(.progress)
102101
.scaledToFit()
103102
#else
104103
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in

Diff for: README.md

+12-14
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

Diff for: SDWebImageSwiftUI/Classes/AnimatedImage.swift

+39-53
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
}
@@ -780,30 +777,19 @@ extension AnimatedImage {
780777
}
781778
}
782779

780+
// Convenient dot syntax
781+
extension SDWebImageIndicator where Self == SDWebImageActivityIndicator {
782+
public static var activity: Self { Self() }
783+
}
784+
785+
extension SDWebImageIndicator where Self == SDWebImageProgressIndicator {
786+
public static var progress: Self { Self() }
787+
}
788+
783789
// Web Image convenience, based on UIKit/AppKit API
784790
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
785791
extension AnimatedImage {
786792

787-
/// Associate a placeholder when loading image with url
788-
/// - Parameter content: A view that describes the placeholder.
789-
/// - 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
790-
public func placeholder<T>(@ViewBuilder content: () -> T) -> AnimatedImage where T : View {
791-
#if os(macOS)
792-
let hostingView = NSHostingView(rootView: content())
793-
#else
794-
let hostingView = _UIHostingView(rootView: content())
795-
#endif
796-
self.imageConfiguration.placeholderView = hostingView
797-
return self
798-
}
799-
800-
/// Associate a placeholder image when loading image with url
801-
/// - Parameter content: A view that describes the placeholder.
802-
public func placeholder(_ image: PlatformImage?) -> AnimatedImage {
803-
self.imageConfiguration.placeholder = image
804-
return self
805-
}
806-
807793
/// Associate a indicator when loading image with url
808794
/// - Note: If you do not need indicator, specify nil. Defaults to nil
809795
/// - Parameter indicator: indicator, see more in `SDWebImageIndicator`

Diff for: Tests/AnimatedImageTests.swift

+4-6
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ class AnimatedImageTests: XCTestCase {
142142
func testAnimatedImageModifier() throws {
143143
let expectation = self.expectation(description: "WebImage modifier")
144144
let imageUrl = URL(string: "https://assets.sbnation.com/assets/2512203/dogflops.gif")
145-
let imageView = AnimatedImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1])
145+
let imageView = AnimatedImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) {
146+
Circle()
147+
}
146148
let introspectView = imageView
147149
.onSuccess { _, _, _ in
148150
expectation.fulfill()
@@ -161,11 +163,7 @@ class AnimatedImageTests: XCTestCase {
161163
XCTAssert(view.isKind(of: SDAnimatedImageView.self))
162164
XCTAssertEqual(context.coordinator.userInfo?["foo"] as? String, "bar")
163165
}
164-
.placeholder(PlatformImage())
165-
.placeholder {
166-
Circle()
167-
}
168-
.indicator(SDWebImageActivityIndicator.medium)
166+
.indicator(.activity)
169167
// Image
170168
.resizable()
171169
.renderingMode(.original)

Diff for: Tests/WebImageTests.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ class WebImageTests: XCTestCase {
7373
func testWebImageModifier() throws {
7474
let expectation = self.expectation(description: "WebImage modifier")
7575
let imageUrl = URL(string: "https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg")
76-
let imageView = WebImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1])
76+
let imageView = WebImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) { image in
77+
image.resizable()
78+
} placeholder: {
79+
Circle()
80+
}
7781
let introspectView = imageView
7882
.onSuccess { _, _, _ in
7983
expectation.fulfill()
@@ -83,10 +87,6 @@ class WebImageTests: XCTestCase {
8387
}
8488
.onProgress { _, _ in
8589

86-
}
87-
.placeholder(.init(platformImage: PlatformImage()))
88-
.placeholder {
89-
Circle()
9090
}
9191
// Image
9292
.resizable()

0 commit comments

Comments
 (0)