Skip to content

Refactory placeholder to support custom View Builder #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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