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:
DreamPiggy 2023-09-20 18:08:31 +08:00
parent 86b1901e40
commit 9ec9e29e14
2 changed files with 85 additions and 70 deletions

View File

@ -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()
}
}
}

View File

@ -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 {