Merge pull request #275 from SDWebImage/api/asyncimage
Update the WebImage API to match SwiftUI.AsyncImage
This commit is contained in:
commit
eb0eafa520
|
@ -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()
|
||||
|
@ -199,15 +191,16 @@ struct ContentView: View {
|
|||
}
|
||||
#endif
|
||||
#if os(watchOS)
|
||||
return contentView()
|
||||
.contextMenu {
|
||||
Button(action: { self.reloadCache() }) {
|
||||
Text("Reload")
|
||||
return NavigationView {
|
||||
contentView()
|
||||
.navigationTitle("WebImage")
|
||||
.toolbar {
|
||||
Button(action: { self.reloadCache() }) {
|
||||
Text("Reload")
|
||||
}
|
||||
}
|
||||
Button(action: { self.switchView() }) {
|
||||
Text("Switch")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
@ -52,10 +52,8 @@ struct DetailView: View {
|
|||
#endif
|
||||
#if os(macOS) || os(watchOS)
|
||||
zoomView()
|
||||
.contextMenu {
|
||||
Button(isAnimating ? "Stop" : "Start") {
|
||||
self.isAnimating.toggle()
|
||||
}
|
||||
.onTapGesture {
|
||||
self.isAnimating.toggle()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -95,24 +93,31 @@ 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)
|
||||
.resizable()
|
||||
.placeholder(.wifiExclamationmark)
|
||||
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
|
||||
image.resizable()
|
||||
.scaledToFit()
|
||||
} placeholder: {
|
||||
Image.wifiExclamationmark
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
.indicator(.progress)
|
||||
.scaledToFit()
|
||||
#endif
|
||||
} else {
|
||||
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating)
|
||||
.resizable()
|
||||
.placeholder(.wifiExclamationmark)
|
||||
WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
|
||||
image.resizable()
|
||||
.scaledToFit()
|
||||
} placeholder: {
|
||||
Image.wifiExclamationmark
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
.indicator(.progress(style: .circular))
|
||||
.scaledToFit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
30
README.md
30
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
|
||||
|
@ -624,8 +622,8 @@ Since SwiftUI is aimed to support all Apple platforms, our demo does this as wel
|
|||
|
||||
Demo Tips:
|
||||
|
||||
1. Use `Switch` (right-click on macOS/force press on watchOS) to switch between `WebImage` and `AnimatedImage`.
|
||||
2. Use `Reload` (right-click on macOS/force press on watchOS) to clear cache.
|
||||
1. Use `Switch` (right-click on macOS/tap on watchOS) to switch between `WebImage` and `AnimatedImage`.
|
||||
2. Use `Reload` (right-click on macOS/button on watchOS) to clear cache.
|
||||
3. Use `Swipe Left` (menu button on tvOS) to delete one image url from list.
|
||||
4. Pinch gesture (Digital Crown on watchOS, play button on tvOS) to zoom-in detail page image.
|
||||
5. Clear cache and go to detail page to see progressive loading.
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -794,30 +791,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`
|
||||
|
@ -835,23 +821,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 {
|
||||
|
|
|
@ -9,6 +9,43 @@
|
|||
import SwiftUI
|
||||
import SDWebImage
|
||||
|
||||
public enum WebImagePhase {
|
||||
/// No image is loaded.
|
||||
case empty
|
||||
|
||||
/// An image succesfully loaded.
|
||||
case success(Image)
|
||||
|
||||
/// An image failed to load with an error.
|
||||
case failure(Error)
|
||||
|
||||
/// The loaded image, if any.
|
||||
///
|
||||
/// If this value isn't `nil`, the image load operation has finished,
|
||||
/// and you can use the image to update the view. You can use the image
|
||||
/// directly, or you can modify it in some way. For example, you can add
|
||||
/// a ``Image/resizable(capInsets:resizingMode:)`` modifier to make the
|
||||
/// image resizable.
|
||||
public var image: Image? {
|
||||
switch self {
|
||||
case let .success(image):
|
||||
return image
|
||||
case .empty, .failure:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The error that occurred when attempting to load an image, if any.
|
||||
public var error: Error? {
|
||||
switch self {
|
||||
case .empty, .success:
|
||||
return nil
|
||||
case let .failure(error):
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data Binding Object, only properties in this object can support changes from user with @State and refresh
|
||||
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
final class WebImageModel : ObservableObject {
|
||||
|
@ -43,10 +80,12 @@ final class WebImageConfiguration: ObservableObject {
|
|||
|
||||
/// A Image View type to load image from url. Supports static/animated image format.
|
||||
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
public struct WebImage : View {
|
||||
public struct WebImage<Content> : View where Content: View {
|
||||
var transaction: Transaction
|
||||
|
||||
var configurations: [(Image) -> Image] = []
|
||||
|
||||
var placeholder: AnyView?
|
||||
var content: (WebImagePhase) -> Content
|
||||
|
||||
/// A Binding to control the animation. You can bind external logic to control the animation status.
|
||||
/// True to start animation, false to stop animation.
|
||||
|
@ -72,7 +111,23 @@ public struct WebImage : View {
|
|||
/// - 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. 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.
|
||||
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
|
||||
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true)) where Content == Image {
|
||||
self.init(url: url, options: options, context: context, isAnimating: isAnimating) { phase in
|
||||
phase.image ?? Image(platformImage: .empty)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
self.init(url: url, options: options, context: context, isAnimating: isAnimating) { phase in
|
||||
if let i = phase.image {
|
||||
content(i)
|
||||
} else {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool> = .constant(true), transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (WebImagePhase) -> Content) {
|
||||
self._isAnimating = isAnimating
|
||||
var context = context ?? [:]
|
||||
// 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 {
|
|||
let imageManager = ImageManager()
|
||||
_imageManager = StateObject(wrappedValue: imageManager)
|
||||
_indicatorStatus = ObservedObject(wrappedValue: imageManager.indicatorStatus)
|
||||
}
|
||||
|
||||
/// Create a web image with url, placeholder, custom options and context.
|
||||
/// - Parameter url: The image url
|
||||
/// - 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))
|
||||
|
||||
self.transaction = transaction
|
||||
self.content = { phase in
|
||||
content(phase)
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
// Container
|
||||
return ZStack {
|
||||
// This empty Image is used to receive container's level appear/disappear to start/stop player, reduce CPU usage
|
||||
Image(platformImage: .empty)
|
||||
.onAppear {
|
||||
self.appearAction()
|
||||
}
|
||||
.onDisappear {
|
||||
self.disappearAction()
|
||||
}
|
||||
// Render Logic for actual animated image frame or static image
|
||||
if imageManager.image != nil && imageModel.url == imageManager.currentURL {
|
||||
if isAnimating && !imageManager.isIncremental {
|
||||
|
@ -118,8 +162,8 @@ public struct WebImage : View {
|
|||
displayImage()
|
||||
}
|
||||
} else {
|
||||
content((imageManager.error != nil) ? .failure(imageManager.error!) : .empty)
|
||||
// Load Logic
|
||||
setupPlaceholder()
|
||||
.onPlatformAppear(appear: {
|
||||
self.setupManager()
|
||||
if (self.imageManager.error == nil) {
|
||||
|
@ -145,7 +189,7 @@ public struct WebImage : View {
|
|||
/// Configure the platform image into the SwiftUI rendering image
|
||||
func configure(image: PlatformImage) -> some View {
|
||||
// Actual rendering SwiftUI image
|
||||
let result: Image
|
||||
var result: Image
|
||||
// NSImage works well with SwiftUI, include Vector and EXIF images.
|
||||
#if os(macOS)
|
||||
result = Image(nsImage: image)
|
||||
|
@ -188,9 +232,12 @@ public struct WebImage : View {
|
|||
|
||||
// Should not use `EmptyView`, which does not respect to the container's frame modifier
|
||||
// Using a empty image instead for better compatible
|
||||
return configurations.reduce(result) { (previous, configuration) in
|
||||
let i = configurations.reduce(result) { (previous, configuration) in
|
||||
configuration(previous)
|
||||
}
|
||||
|
||||
// Apply view builder
|
||||
return content(.success(i))
|
||||
}
|
||||
|
||||
/// Image Manager status
|
||||
|
@ -279,25 +326,6 @@ public struct WebImage : View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder View Support
|
||||
func setupPlaceholder() -> some View {
|
||||
// Don't use `Group` because it will trigger `.onAppear` and `.onDisappear` when condition view removed, treat placeholder as an entire component
|
||||
let result: AnyView
|
||||
if let placeholder = placeholder {
|
||||
// If use `.delayPlaceholder`, the placeholder is applied after loading failed, hide during loading :)
|
||||
if imageModel.options.contains(.delayPlaceholder) && imageManager.error == nil {
|
||||
result = AnyView(configure(image: .empty))
|
||||
} else {
|
||||
result = placeholder
|
||||
}
|
||||
} else {
|
||||
result = AnyView(configure(image: .empty))
|
||||
}
|
||||
// Custom ID to avoid SwiftUI engine cache the status, and does not call `onAppear` when placeholder not changed (See `ContentView.swift/ContentView2` case)
|
||||
// Because we load the image url in placeholder's `onAppear`, it should be called to sync with state changes :)
|
||||
return result.id(imageModel.url)
|
||||
}
|
||||
}
|
||||
|
||||
// Layout
|
||||
|
@ -373,27 +401,6 @@ extension WebImage {
|
|||
// WebImage Modifier
|
||||
@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension WebImage {
|
||||
|
||||
/// Associate a placeholder when loading image with url
|
||||
/// - note: The differences between Placeholder and Indicator, is that placeholder does not supports animation, and return type is different
|
||||
/// - Parameter content: A view that describes the placeholder.
|
||||
public func placeholder<T>(@ViewBuilder content: () -> T) -> WebImage where T : View {
|
||||
var result = self
|
||||
result.placeholder = AnyView(content())
|
||||
return result
|
||||
}
|
||||
|
||||
/// Associate a placeholder image when loading image with url
|
||||
/// - 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
|
||||
/// - Parameter image: A Image view that describes the placeholder.
|
||||
public func placeholder(_ image: Image) -> WebImage {
|
||||
return placeholder {
|
||||
configurations.reduce(image) { (previous, configuration) in
|
||||
configuration(previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Control the behavior to retry the failed loading when view become appears again
|
||||
/// - Parameter flag: Whether or not to retry the failed loading
|
||||
public func retryOnAppear(_ flag: Bool) -> WebImage {
|
||||
|
|
|
@ -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