Merge pull request #84 from SDWebImage/support_indicator_modifier_animated_image

Support indicator modifier animated image
This commit is contained in:
DreamPiggy 2020-03-03 21:39:20 +08:00 committed by GitHub
commit ca22dc45a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 73 additions and 14 deletions

View File

@ -178,6 +178,16 @@ var body: some View {
Note: `AnimatedImage` supports both image url or image data for animated image format. Which use the SDWebImage's [Animated ImageView](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) for internal implementation. Pay attention that since this base on UIKit/AppKit representable, some advanced SwiftUI layout and animation system may not work as expected. You may need UIKit/AppKit and Core Animation to modify the native view.
Note: `AnimatedImage` some methods like `.transition`, `.indicator` and `.aspectRatio` have the same naming as `SwiftUI.View` protocol methods. But the args receive the different type. This is because `AnimatedImage` supports to be used with UIKit/AppKit component and animation. If you find ambiguity, use full type declaration instead of the dot expression syntax.
```swift
AnimatedImage(name: "animation2") // Just for showcase, don't mix them at the same time
.indicator(SDWebImageProgressIndicator.default) // UIKit indicator component
.indicator(Indicator.progress) // SwiftUI indicator component
.transition(SDWebImageTransition.flipFromLeft) // UIKit animation transition
.transition(AnyTransition.flipFromLeft) // SwiftUI animation transition
```
### Which View to choose
Why we have two different View types here, is because of current SwiftUI limit. But we're aimed to provide best solution for all use cases.

View File

@ -11,7 +11,7 @@ import SDWebImage
#if os(iOS) || os(tvOS) || os(macOS)
/// A coordinator object used for `AnimatedImage`native view bridge for UIKit/AppKit/WatchKit.
/// A coordinator object used for `AnimatedImage`native view bridge for UIKit/AppKit.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public final class AnimatedImageCoordinator: NSObject {
@ -37,6 +37,14 @@ final class AnimatedImageModel : ObservableObject {
@Published var scale: CGFloat = 1
}
/// Loading Binding Object, only properties in this object can support changes from user with @State and refresh
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final class AnimatedLoadingModel : ObservableObject, IndicatorReportable {
@Published var image: PlatformImage? // loaded image, note when progressive loading, this will published multiple times with different partial image
@Published var isLoading: Bool = false // whether network is loading or cache is querying, should only be used for indicator binding
@Published var progress: Double = 0 // network progress, should only be used for indicator binding
}
/// Completion Handler Binding Object, supports dynamic @State changes
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final class AnimatedImageHandler: ObservableObject {
@ -81,6 +89,7 @@ final class AnimatedImageConfiguration: ObservableObject {
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct AnimatedImage : PlatformViewRepresentable {
@ObservedObject var imageModel = AnimatedImageModel()
@ObservedObject var imageLoading = AnimatedLoadingModel()
@ObservedObject var imageHandler = AnimatedImageHandler()
@ObservedObject var imageLayout = AnimatedImageLayout()
@ObservedObject var imageConfiguration = AnimatedImageConfiguration()
@ -193,11 +202,21 @@ public struct AnimatedImage : PlatformViewRepresentable {
if currentOperation != nil {
return
}
self.imageLoading.isLoading = true
view.wrapped.sd_setImage(with: imageModel.url, placeholderImage: imageConfiguration.placeholder, options: imageModel.webOptions, context: imageModel.webContext, progress: { (receivedSize, expectedSize, _) in
let progress: Double
if (expectedSize > 0) {
progress = Double(receivedSize) / Double(expectedSize)
} else {
progress = 0
}
DispatchQueue.main.async {
self.imageLoading.progress = progress
}
self.imageHandler.progressBlock?(receivedSize, expectedSize)
}) { (image, error, cacheType, _) in
// This is a hack because of Xcode 11.3 bug, the @Published does not trigger another `updateUIView` call
// Here I have to use UIKit API to triger the same effect (the window change implicitly cause re-render)
// Here I have to use UIKit/AppKit API to triger the same effect (the window change implicitly cause re-render)
if let hostingView = AnimatedImage.findHostingView(from: view) {
#if os(macOS)
hostingView.viewDidMoveToWindow()
@ -205,6 +224,9 @@ public struct AnimatedImage : PlatformViewRepresentable {
hostingView.didMoveToWindow()
#endif
}
self.imageLoading.image = image
self.imageLoading.isLoading = false
self.imageLoading.progress = 1
if let image = image {
self.imageHandler.successBlock?(image, cacheType)
} else {
@ -704,7 +726,7 @@ extension AnimatedImage {
}
}
// Web Image convenience
// Web Image convenience, based on UIKit/AppKit API
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension AnimatedImage {
@ -732,6 +754,23 @@ extension AnimatedImage {
}
}
// Indicator
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension AnimatedImage {
/// Associate a indicator when loading image with url
/// - Parameter indicator: The indicator type, see `Indicator`
public func indicator<T>(_ indicator: Indicator<T>) -> some View where T : View {
return self.modifier(IndicatorViewModifier(reporter: self.imageLoading, indicator: indicator))
}
/// Associate a indicator when loading image with url, convenient method with block
/// - Parameter content: A view that describes the indicator.
public func indicator<T>(@ViewBuilder content: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<Double>) -> T) -> some View where T : View {
return indicator(Indicator(content: content))
}
}
#if DEBUG
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
struct AnimatedImage_Previews : PreviewProvider {

View File

@ -10,7 +10,7 @@ import SwiftUI
import SDWebImage
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
class ImageManager : ObservableObject {
class ImageManager : ObservableObject, IndicatorReportable {
@Published var image: PlatformImage? // loaded image, note when progressive loading, this will published multiple times with different partial image
@Published var isLoading: Bool = false // whether network is loading or cache is querying, should only be used for indicator binding
@Published var progress: Double = 0 // network progress, should only be used for indicator binding
@ -70,9 +70,7 @@ class ImageManager : ObservableObject {
// So previous View struct call `onDisappear` and cancel the currentOperation
return
}
if let image = image {
self.image = image
}
self.image = image
self.isIncremental = !finished
if finished {
self.isLoading = false

View File

@ -17,27 +17,39 @@ public struct Indicator<T> where T : View {
/// 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. Value between [0, 1]. If no progress can be reported, the value is 0.
/// - Parameter progress: A Binding to control the progress during loading. Value between [0.0, 1.0]. If no progress can be reported, the value is 0.
/// Associate a indicator when loading image with url
public init(@ViewBuilder content: @escaping (_ isAnimating: Binding<Bool>, _ progress: Binding<Double>) -> T) {
self.content = content
}
}
/// A protocol to report indicator progress
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol IndicatorReportable : ObservableObject {
/// whether indicator is loading or not
var isLoading: Bool { get set }
/// indicator progress, should only be used for indicator binding, value between [0.0, 1.0]
var progress: Double { get set }
}
/// A implementation detail View Modifier with indicator
/// SwiftUI View Modifier construced by using a internal View type which modify the `body`
/// It use type system to represent the view hierarchy, and Swift `some View` syntax to hide the type detail for users
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
struct IndicatorViewModifier<T> : ViewModifier where T : View {
@ObservedObject var imageManager: ImageManager
public struct IndicatorViewModifier<T, V> : ViewModifier where T : View, V : IndicatorReportable {
/// The progress reporter
@ObservedObject var reporter: V
/// The indicator
var indicator: Indicator<T>
func body(content: Content) -> some View {
public func body(content: Content) -> some View {
ZStack {
content
if imageManager.isLoading {
indicator.content($imageManager.isLoading, $imageManager.progress)
if reporter.isLoading {
indicator.content($reporter.isLoading, $reporter.progress)
}
}
}

View File

@ -280,7 +280,7 @@ extension WebImage {
/// Associate a indicator when loading image with url
/// - Parameter indicator: The indicator type, see `Indicator`
public func indicator<T>(_ indicator: Indicator<T>) -> some View where T : View {
return self.modifier(IndicatorViewModifier(imageManager: imageManager, indicator: indicator))
return self.modifier(IndicatorViewModifier(reporter: imageManager, indicator: indicator))
}
/// Associate a indicator when loading image with url, convenient method with block