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
This commit is contained in:
parent
86b1901e40
commit
9ec9e29e14
|
@ -101,18 +101,26 @@ struct DetailView: View {
|
|||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue