Skip to content
8 changes: 8 additions & 0 deletions Example/SDWebImageSwiftUIDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ struct ContentView: View {
#if os(macOS) || os(iOS) || os(tvOS)
AnimatedImage(url: URL(string:url))
.indicator(SDWebImageActivityIndicator.medium)
/**
.placeholder(UIImage(systemName: "photo"))
*/
.transition(.fade)
.resizable()
.scaledToFit()
Expand All @@ -100,6 +103,11 @@ struct ContentView: View {
#if os(macOS) || os(iOS) || os(tvOS)
WebImage(url: URL(string:url))
.resizable()
/**
.placeholder {
Image(systemName: "photo")
}
*/
.indicator(.activity)
.animation(.easeInOut(duration: 0.5))
.transition(.fade)
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ var body: some View {
// Success
}
.resizable() // Resizable like SwiftUI.Image
.placeholder {
Image(systemName: "photo") // Placeholder
}
.indicator(.activity) // Activity Indicator
.animation(.easeInOut(duration: 0.5))
.animation(.easeInOut(duration: 0.5)) // Animation Duration
.transition(.fade) // Fade Transition
.scaledToFit()
.frame(width: 300, height: 300, alignment: .center)
Expand All @@ -118,6 +121,7 @@ var body: some View {
// Error
}
.resizable() // Actually this is not needed unlike SwiftUI.Image
.placeholder(UIImage(systemName: "photo")) // Placeholder
.indicator(SDWebImageActivityIndicator.medium) // Activity Indicator
.transition(.fade) // Fade Transition
.scaledToFit() // Attention to call it on AnimatedImage, but not `some View` after View Modifier
Expand Down
36 changes: 22 additions & 14 deletions SDWebImageSwiftUI/Classes/AnimatedImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ final class AnimatedImageConfiguration: ObservableObject {
@Published var indicator: SDWebImageIndicator?
@Published var transition: SDWebImageTransition?
#endif
@Published var placeholder: PlatformImage?
}

// Convenient
Expand All @@ -67,7 +68,6 @@ public struct AnimatedImage : PlatformViewRepresentable {
@ObservedObject var imageCoordinator = AnimatedImageCoordinator()

var url: URL?
var placeholder: PlatformImage?
var webOptions: SDWebImageOptions = []
var webContext: [SDWebImageContextOption : Any]? = nil

Expand All @@ -80,8 +80,8 @@ public struct AnimatedImage : PlatformViewRepresentable {
/// - 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?, placeholder: PlatformImage? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
self.init(url: url, placeholder: placeholder, options: options, context: context, isAnimating: .constant(true))
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
self.init(url: url, options: options, context: context, isAnimating: .constant(true))
}

/// Create an animated image with url, placeholder, custom options and context, including animation control binding.
Expand All @@ -90,9 +90,8 @@ 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?, placeholder: PlatformImage? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding<Bool>) {
self._isAnimating = isAnimating
self.placeholder = placeholder
self.webOptions = options
self.webContext = context
self.url = url
Expand Down Expand Up @@ -190,7 +189,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
if currentOperation != nil {
return
}
view.wrapped.sd_setImage(with: url, placeholderImage: placeholder, options: webOptions, context: webContext, progress: { (receivedSize, expectedSize, _) in
view.wrapped.sd_setImage(with: url, placeholderImage: imageConfiguration.placeholder, options: webOptions, context: webContext, progress: { (receivedSize, expectedSize, _) in
self.imageModel.progressBlock?(receivedSize, expectedSize)
}) { (image, error, cacheType, _) in
if let image = image {
Expand Down Expand Up @@ -522,12 +521,14 @@ extension AnimatedImage {
#if os(macOS) || os(iOS) || os(tvOS)
return self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)
#else
if let aspectRatio = aspectRatio {
return AnyView(self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode))
} else {
// on watchOS, there are no workaround like `AnimatedImageViewWrapper` to override `intrinsicContentSize`, so the aspect ratio is undetermined and cause sizing issues
// To workaround, we do not call default implementation for this case, using original solution instead
return AnyView(self)
return Group {
if aspectRatio != nil {
self.modifier(EmptyModifier()).aspectRatio(aspectRatio, contentMode: contentMode)
} else {
// on watchOS, there are no workaround like `AnimatedImageViewWrapper` to override `intrinsicContentSize`, so the aspect ratio is undetermined and cause sizing issues
// To workaround, we do not call default implementation for this case, using original solution instead
self
}
}
#endif
}
Expand Down Expand Up @@ -650,10 +651,17 @@ extension AnimatedImage {
}
}

#if os(macOS) || os(iOS) || os(tvOS)
// Web Image convenience
extension AnimatedImage {

/// Associate a placeholder when loading image with url
/// - Parameter content: A view that describes the placeholder.
public func placeholder(_ placeholder: PlatformImage?) -> AnimatedImage {
imageConfiguration.placeholder = placeholder
return self
}

#if os(macOS) || os(iOS) || os(tvOS)
/// 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`
Expand All @@ -669,8 +677,8 @@ extension AnimatedImage {
imageConfiguration.transition = transition
return self
}
#endif
}
#endif

#if DEBUG
struct AnimatedImage_Previews : PreviewProvider {
Expand Down
22 changes: 11 additions & 11 deletions SDWebImageSwiftUI/Classes/Indicator/Indicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import SwiftUI

/// A type to build the indicator
public struct Indicator<T> where T : View {
var builder: (Binding<Bool>, Binding<CGFloat>) -> T
var content: (Binding<Bool>, Binding<CGFloat>) -> T

/// Create a indicator with builder
/// - Parameter builder: A builder to build indicator
/// - Parameter isAnimating: A Binding to control the animation. If image is during loading, the value is true, else (like start loading) the value is false.
/// - Parameter progress: A Binding to control the progress during loading. If no progress can be reported, the value is 0.
/// Associate a indicator when loading image with url
public init(@ViewBuilder builder: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<CGFloat>) -> T) {
self.builder = builder
public init(@ViewBuilder content: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<CGFloat>) -> T) {
self.content = content
}
}

Expand All @@ -32,17 +32,17 @@ struct IndicatorViewModifier<T> : ViewModifier where T : View {
var indicator: Indicator<T>

func body(content: Content) -> some View {
if imageManager.isFinished {
// Disable Indiactor
return AnyView(content)
} else {
// Enable indicator
return AnyView(
Group {
if imageManager.isFinished {
// Disable Indiactor
content
} else {
// Enable indicator
ZStack {
content
indicator.builder($imageManager.isLoading, $imageManager.progress)
indicator.content($imageManager.isLoading, $imageManager.progress)
}
)
}
}
}
}
Expand Down
90 changes: 61 additions & 29 deletions SDWebImageSwiftUI/Classes/WebImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,23 @@ public struct WebImage : View {
static var emptyImage = PlatformImage()

var url: URL?
var placeholder: Image?
var options: SDWebImageOptions
var context: [SDWebImageContextOption : Any]?

var configurations: [(Image) -> Image] = []

var placeholder: AnyView?
var retryOnAppear: Bool = true
var cancelOnDisappear: Bool = true

@ObservedObject var imageManager: ImageManager

/// Create a web image with url, placeholder, custom options and context.
/// - 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?, placeholder: Image? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
self.url = url
self.placeholder = placeholder
self.options = options
self.context = context
self.imageManager = ImageManager(url: url, options: options, context: context)
Expand All @@ -38,31 +39,33 @@ public struct WebImage : View {
}

public var body: some View {
if let platformImage = imageManager.image {
var image = Image(platformImage: platformImage)
image = configurations.reduce(image) { (previous, configuration) in
configuration(previous)
}
let view = image
return AnyView(view)
} else {
var image = placeholder ?? Image(platformImage: WebImage.emptyImage)
image = configurations.reduce(image) { (previous, configuration) in
configuration(previous)
}
let view = image
.onAppear {
if !self.imageManager.isFinished {
self.imageManager.load()
Group {
if imageManager.image != nil {
configurations.reduce(Image(platformImage: imageManager.image!)) { (previous, configuration) in
configuration(previous)
}
}
.onDisappear {
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
if self.imageManager.isLoading && !self.imageManager.isIncremental {
self.imageManager.cancel()
} else {
Group {
if placeholder != nil {
placeholder
} else {
Image(platformImage: WebImage.emptyImage)
}
}
.onAppear {
guard self.retryOnAppear else { return }
if !self.imageManager.isFinished {
self.imageManager.load()
}
}
.onDisappear {
guard self.cancelOnDisappear else { return }
// When using prorgessive loading, the previous partial image will cause onDisappear. Filter this case
if self.imageManager.isLoading && !self.imageManager.isIncremental {
self.imageManager.cancel()
}
}
}
return AnyView(view)
}
}
}
Expand Down Expand Up @@ -135,6 +138,35 @@ extension WebImage {
}
}

// WebImage Modifier
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
}

/// 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 {
var result = self
result.retryOnAppear = flag
return result
}

/// Control the behavior to cancel the pending loading when view become disappear again
/// - Parameter flag: Whether or not to cancel the pending loading
public func cancelOnDisappear(_ flag: Bool) -> WebImage {
var result = self
result.cancelOnDisappear = flag
return result
}
}

// Indicator
extension WebImage {

Expand All @@ -145,9 +177,9 @@ extension WebImage {
}

/// Associate a indicator when loading image with url, convenient method with block
/// - Parameter indicator: The indicator type, see `Indicator`
public func indicator<T>(@ViewBuilder builder: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<CGFloat>) -> T) -> some View where T : View {
return indicator(Indicator(builder: builder))
/// - Parameter content: A view that describes the indicator.
public func indicator<T>(@ViewBuilder content: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<CGFloat>) -> T) -> some View where T : View {
return indicator(Indicator(content: content))
}
}

Expand Down