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
This commit is contained in:
parent
9ec9e29e14
commit
a94221fba0
|
@ -96,22 +96,19 @@ struct ContentView: View {
|
|||
HStack {
|
||||
if self.animated {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS)
|
||||
AnimatedImage(url: URL(string:url), isAnimating: .constant(true))
|
||||
AnimatedImage(url: URL(string:url))
|
||||
.onViewUpdate { view, context in
|
||||
#if os(macOS)
|
||||
view.toolTip = url
|
||||
#endif
|
||||
}
|
||||
.indicator(SDWebImageActivityIndicator.medium)
|
||||
/**
|
||||
.placeholder(UIImage(systemName: "photo"))
|
||||
*/
|
||||
.indicator(.activity)
|
||||
.transition(.fade)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
|
||||
#else
|
||||
WebImage(url: URL(string:url), isAnimating: self.$animated)
|
||||
WebImage(url: URL(string:url))
|
||||
.resizable()
|
||||
.indicator(.activity)
|
||||
.transition(.fade(duration: 0.5))
|
||||
|
@ -119,13 +116,8 @@ struct ContentView: View {
|
|||
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
|
||||
#endif
|
||||
} else {
|
||||
WebImage(url: URL(string:url), isAnimating: .constant(true))
|
||||
WebImage(url: URL(string:url))
|
||||
.resizable()
|
||||
/**
|
||||
.placeholder {
|
||||
Image(systemName: "photo")
|
||||
}
|
||||
*/
|
||||
.indicator(.activity)
|
||||
.transition(.fade(duration: 0.5))
|
||||
.scaledToFit()
|
||||
|
|
|
@ -95,10 +95,9 @@ struct DetailView: View {
|
|||
HStack {
|
||||
if animated {
|
||||
#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS)
|
||||
AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating)
|
||||
AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating, placeholderImage: .wifiExclamationmark)
|
||||
.indicator(.progress)
|
||||
.resizable()
|
||||
.placeholder(.wifiExclamationmark)
|
||||
.indicator(SDWebImageProgressIndicator.default)
|
||||
.scaledToFit()
|
||||
#else
|
||||
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
|
||||
|
|
26
README.md
26
README.md
|
@ -128,18 +128,16 @@ github "SDWebImage/SDWebImageSwiftUI"
|
|||
|
||||
```swift
|
||||
var body: some View {
|
||||
WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic"))
|
||||
WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic")) { image in
|
||||
image.resizable() // Control layout like SwiftUI.AsyncImage, you must use this modifier or the view will use the image bitmap size
|
||||
} placeholder: {
|
||||
Rectangle().foregroundColor(.gray)
|
||||
}
|
||||
// Supports options and context, like `.delayPlaceholder` to show placeholder only when error
|
||||
.onSuccess { image, data, cacheType in
|
||||
// Success
|
||||
// Note: Data exist only when queried from disk cache or network. Use `.queryMemoryData` if you really need data
|
||||
}
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder(Image(systemName: "photo")) // Placeholder Image
|
||||
// Supports ViewBuilder as well
|
||||
.placeholder {
|
||||
Rectangle().foregroundColor(.gray)
|
||||
}
|
||||
.indicator(.activity) // Activity Indicator
|
||||
.transition(.fade(duration: 0.5)) // Fade Transition with duration
|
||||
.scaledToFit()
|
||||
|
@ -194,21 +192,21 @@ WebImage(url: url)
|
|||
```swift
|
||||
var body: some View {
|
||||
Group {
|
||||
AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"))
|
||||
AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"), placeholderImage: .init(systemName: "photo")) // Placeholder Image
|
||||
// Supports options and context, like `.progressiveLoad` for progressive animation loading
|
||||
.onFailure { error in
|
||||
// Error
|
||||
}
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder(UIImage(systemName: "photo")) // Placeholder Image
|
||||
// Supports ViewBuilder as well
|
||||
.placeholder {
|
||||
Circle().foregroundColor(.gray)
|
||||
}
|
||||
.indicator(SDWebImageActivityIndicator.medium) // Activity Indicator
|
||||
.indicator(.activity) // Activity Indicator
|
||||
.transition(.fade) // Fade Transition
|
||||
.scaledToFit() // Attention to call it on AnimatedImage, but not `some View` after View Modifier (Swift Protocol Extension method is static dispatched)
|
||||
|
||||
// Supports SwiftUI ViewBuilder placeholder as well
|
||||
AnimatedImage(url: url) {
|
||||
Circle().foregroundColor(.gray)
|
||||
}
|
||||
|
||||
// Data
|
||||
AnimatedImage(data: try! Data(contentsOf: URL(fileURLWithPath: "/tmp/foo.webp")))
|
||||
.customLoopCount(1) // Custom loop count
|
||||
|
|
|
@ -31,6 +31,12 @@ final class AnimatedImageModel : ObservableObject {
|
|||
@Published var url: URL?
|
||||
@Published var webOptions: SDWebImageOptions = []
|
||||
@Published var webContext: [SDWebImageContextOption : Any]? = nil
|
||||
@Published var placeholderImage: PlatformImage?
|
||||
@Published var placeholderView: PlatformView? {
|
||||
didSet {
|
||||
oldValue?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
/// Name image
|
||||
@Published var name: String?
|
||||
@Published var bundle: Bundle?
|
||||
|
@ -90,12 +96,6 @@ final class AnimatedImageConfiguration: ObservableObject {
|
|||
// These configurations only useful for web image loading
|
||||
var indicator: SDWebImageIndicator?
|
||||
var transition: SDWebImageTransition?
|
||||
var placeholder: PlatformImage?
|
||||
var placeholderView: PlatformView? {
|
||||
didSet {
|
||||
oldValue?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
|||
/// True to start animation, false to stop animation.
|
||||
@Binding public var isAnimating: Bool
|
||||
|
||||
/// Create an animated image with url, placeholder, custom options and context.
|
||||
/// Create an animated image with url, placeholder, custom options and context, including animation control binding.
|
||||
/// - Parameter url: The image url
|
||||
/// - Parameter placeholder: The placeholder image to show during loading
|
||||
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
|
||||
/// - 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.
|
||||
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
|
||||
self.init(url: url, options: options, context: context, isAnimating: .constant(true))
|
||||
/// - Parameter isAnimating: The binding for animation control
|
||||
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), placeholderImage: PlatformImage? = nil) {
|
||||
let imageModel = AnimatedImageModel()
|
||||
imageModel.url = url
|
||||
imageModel.webOptions = options
|
||||
imageModel.webContext = context
|
||||
imageModel.placeholderImage = placeholderImage
|
||||
self.init(imageModel: imageModel, isAnimating: isAnimating)
|
||||
}
|
||||
|
||||
/// Create an animated image with url, placeholder, custom options and context, including animation control binding.
|
||||
|
@ -130,46 +136,37 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
/// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
|
||||
/// - 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.
|
||||
/// - Parameter isAnimating: The binding for animation control
|
||||
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
|
||||
public init<T>(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), @ViewBuilder placeholder: @escaping () -> T) where T : View {
|
||||
let imageModel = AnimatedImageModel()
|
||||
imageModel.url = url
|
||||
imageModel.webOptions = options
|
||||
imageModel.webContext = context
|
||||
#if os(macOS)
|
||||
let hostingView = NSHostingView(rootView: placeholder())
|
||||
#else
|
||||
let hostingView = _UIHostingView(rootView: placeholder())
|
||||
#endif
|
||||
imageModel.placeholderView = hostingView
|
||||
self.init(imageModel: imageModel, isAnimating: isAnimating)
|
||||
}
|
||||
|
||||
/// Create an animated image with name and bundle.
|
||||
/// - Note: Asset Catalog is not supported.
|
||||
/// - Parameter name: The image name
|
||||
/// - Parameter bundle: The bundle contains image
|
||||
public init(name: String, bundle: Bundle? = nil) {
|
||||
self.init(name: name, bundle: bundle, isAnimating: .constant(true))
|
||||
}
|
||||
|
||||
/// Create an animated image with name and bundle, including animation control binding.
|
||||
/// - Note: Asset Catalog is not supported.
|
||||
/// - Parameter name: The image name
|
||||
/// - Parameter bundle: The bundle contains image
|
||||
/// - Parameter isAnimating: The binding for animation control
|
||||
public init(name: String, bundle: Bundle? = nil, isAnimating: Binding<Bool>) {
|
||||
public init(name: String, bundle: Bundle? = nil, isAnimating: Binding<Bool> = .constant(true)) {
|
||||
let imageModel = AnimatedImageModel()
|
||||
imageModel.name = name
|
||||
imageModel.bundle = bundle
|
||||
self.init(imageModel: imageModel, isAnimating: isAnimating)
|
||||
}
|
||||
|
||||
/// Create an animated image with data and scale.
|
||||
/// - Parameter data: The image data
|
||||
/// - Parameter scale: The scale factor
|
||||
public init(data: Data, scale: CGFloat = 1) {
|
||||
self.init(data: data, scale: scale, isAnimating: .constant(true))
|
||||
}
|
||||
|
||||
/// Create an animated image with data and scale, including animation control binding.
|
||||
/// - Parameter data: The image data
|
||||
/// - Parameter scale: The scale factor
|
||||
/// - Parameter isAnimating: The binding for animation control
|
||||
public init(data: Data, scale: CGFloat = 1, isAnimating: Binding<Bool>) {
|
||||
public init(data: Data, scale: CGFloat = 1, isAnimating: Binding<Bool> = .constant(true)) {
|
||||
let imageModel = AnimatedImageModel()
|
||||
imageModel.data = data
|
||||
imageModel.scale = scale
|
||||
|
@ -222,7 +219,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
func setupIndicator(_ view: AnimatedImageViewWrapper, context: Context) {
|
||||
view.wrapped.sd_imageIndicator = imageConfiguration.indicator
|
||||
view.wrapped.sd_imageTransition = imageConfiguration.transition
|
||||
if let placeholderView = imageConfiguration.placeholderView {
|
||||
if let placeholderView = imageModel.placeholderView {
|
||||
placeholderView.removeFromSuperview()
|
||||
placeholderView.isHidden = true
|
||||
// Placeholder View should below the Indicator View
|
||||
|
@ -243,13 +240,13 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
context.coordinator.imageLoading.isLoading = true
|
||||
let webOptions = imageModel.webOptions
|
||||
if webOptions.contains(.delayPlaceholder) {
|
||||
self.imageConfiguration.placeholderView?.isHidden = true
|
||||
self.imageModel.placeholderView?.isHidden = true
|
||||
} else {
|
||||
self.imageConfiguration.placeholderView?.isHidden = false
|
||||
self.imageModel.placeholderView?.isHidden = false
|
||||
}
|
||||
var webContext = imageModel.webContext ?? [:]
|
||||
webContext[.animatedImageClass] = SDAnimatedImage.self
|
||||
view.wrapped.sd_internalSetImage(with: imageModel.url, placeholderImage: imageConfiguration.placeholder, options: webOptions, context: webContext, setImageBlock: nil, progress: { (receivedSize, expectedSize, _) in
|
||||
view.wrapped.sd_internalSetImage(with: imageModel.url, placeholderImage: imageModel.placeholderImage, options: webOptions, context: webContext, setImageBlock: nil, progress: { (receivedSize, expectedSize, _) in
|
||||
let progress: Double
|
||||
if (expectedSize > 0) {
|
||||
progress = Double(receivedSize) / Double(expectedSize)
|
||||
|
@ -265,10 +262,10 @@ public struct AnimatedImage : PlatformViewRepresentable {
|
|||
context.coordinator.imageLoading.isLoading = false
|
||||
context.coordinator.imageLoading.progress = 1
|
||||
if let image = image {
|
||||
self.imageConfiguration.placeholderView?.isHidden = true
|
||||
self.imageModel.placeholderView?.isHidden = true
|
||||
self.imageHandler.successBlock?(image, data, cacheType)
|
||||
} else {
|
||||
self.imageConfiguration.placeholderView?.isHidden = false
|
||||
self.imageModel.placeholderView?.isHidden = false
|
||||
self.imageHandler.failureBlock?(error ?? NSError())
|
||||
}
|
||||
}
|
||||
|
@ -780,30 +777,19 @@ extension AnimatedImage {
|
|||
}
|
||||
}
|
||||
|
||||
// Convenient indicator dot syntax
|
||||
extension SDWebImageIndicator where Self == SDWebImageActivityIndicator {
|
||||
public static var activity: Self { Self() }
|
||||
}
|
||||
|
||||
extension SDWebImageIndicator where Self == SDWebImageProgressIndicator {
|
||||
public static var progress: Self { Self() }
|
||||
}
|
||||
|
||||
// Web Image convenience, based on UIKit/AppKit API
|
||||
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension AnimatedImage {
|
||||
|
||||
/// Associate a placeholder when loading image with url
|
||||
/// - Parameter content: A view that describes the placeholder.
|
||||
/// - 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
|
||||
public func placeholder<T>(@ViewBuilder content: () -> T) -> AnimatedImage where T : View {
|
||||
#if os(macOS)
|
||||
let hostingView = NSHostingView(rootView: content())
|
||||
#else
|
||||
let hostingView = _UIHostingView(rootView: content())
|
||||
#endif
|
||||
self.imageConfiguration.placeholderView = hostingView
|
||||
return self
|
||||
}
|
||||
|
||||
/// Associate a placeholder image when loading image with url
|
||||
/// - Parameter content: A view that describes the placeholder.
|
||||
public func placeholder(_ image: PlatformImage?) -> AnimatedImage {
|
||||
self.imageConfiguration.placeholder = image
|
||||
return self
|
||||
}
|
||||
|
||||
/// Associate a indicator when loading image with url
|
||||
/// - Note: If you do not need indicator, specify nil. Defaults to nil
|
||||
/// - Parameter indicator: indicator, see more in `SDWebImageIndicator`
|
||||
|
@ -821,23 +807,6 @@ extension AnimatedImage {
|
|||
}
|
||||
}
|
||||
|
||||
// Indicator
|
||||
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension AnimatedImage {
|
||||
|
||||
/// Associate a indicator when loading image with url
|
||||
/// - Parameter indicator: The indicator type, see `Indicator`
|
||||
public func indicator<T>(_ indicator: Indicator<T>) -> some View where T : View {
|
||||
return self.modifier(IndicatorViewModifier(status: indicatorStatus, indicator: indicator))
|
||||
}
|
||||
|
||||
/// Associate a indicator when loading image with url, convenient method with block
|
||||
/// - Parameter content: A view that describes the indicator.
|
||||
public func indicator<T>(@ViewBuilder content: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<Double>) -> T) -> some View where T : View {
|
||||
return indicator(Indicator(content: content))
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
struct AnimatedImage_Previews : PreviewProvider {
|
||||
|
|
|
@ -142,7 +142,9 @@ class AnimatedImageTests: XCTestCase {
|
|||
func testAnimatedImageModifier() throws {
|
||||
let expectation = self.expectation(description: "WebImage modifier")
|
||||
let imageUrl = URL(string: "https://assets.sbnation.com/assets/2512203/dogflops.gif")
|
||||
let imageView = AnimatedImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1])
|
||||
let imageView = AnimatedImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) {
|
||||
Circle()
|
||||
}
|
||||
let introspectView = imageView
|
||||
.onSuccess { _, _, _ in
|
||||
expectation.fulfill()
|
||||
|
@ -161,11 +163,7 @@ class AnimatedImageTests: XCTestCase {
|
|||
XCTAssert(view.isKind(of: SDAnimatedImageView.self))
|
||||
XCTAssertEqual(context.coordinator.userInfo?["foo"] as? String, "bar")
|
||||
}
|
||||
.placeholder(PlatformImage())
|
||||
.placeholder {
|
||||
Circle()
|
||||
}
|
||||
.indicator(SDWebImageActivityIndicator.medium)
|
||||
.indicator(.activity)
|
||||
// Image
|
||||
.resizable()
|
||||
.renderingMode(.original)
|
||||
|
|
|
@ -73,7 +73,11 @@ class WebImageTests: XCTestCase {
|
|||
func testWebImageModifier() throws {
|
||||
let expectation = self.expectation(description: "WebImage modifier")
|
||||
let imageUrl = URL(string: "https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg")
|
||||
let imageView = WebImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1])
|
||||
let imageView = WebImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) { image in
|
||||
image.resizable()
|
||||
} placeholder: {
|
||||
Circle()
|
||||
}
|
||||
let introspectView = imageView
|
||||
.onSuccess { _, _, _ in
|
||||
expectation.fulfill()
|
||||
|
@ -83,10 +87,6 @@ class WebImageTests: XCTestCase {
|
|||
}
|
||||
.onProgress { _, _ in
|
||||
|
||||
}
|
||||
.placeholder(.init(platformImage: PlatformImage()))
|
||||
.placeholder {
|
||||
Circle()
|
||||
}
|
||||
// Image
|
||||
.resizable()
|
||||
|
|
Loading…
Reference in New Issue