Skip to content

Commit

Permalink
Update the WebImage API to match SwiftUI.AsyncImage (not SwiftUI.Imag…
Browse files Browse the repository at this point in the history
…e), make it more easy to replace

The old API is still kept, except the .placeholder one
  • Loading branch information
dreampiggy committed Sep 21, 2023
1 parent 86b1901 commit 9ec9e29
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 70 deletions.
24 changes: 16 additions & 8 deletions Example/SDWebImageSwiftUIDemo/DetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
Expand Down
131 changes: 69 additions & 62 deletions SDWebImageSwiftUI/Classes/WebImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 9ec9e29

Please sign in to comment.