Merge pull request #84 from SDWebImage/support_indicator_modifier_animated_image
Support indicator modifier animated image
This commit is contained in:
commit
ca22dc45a6
10
README.md
10
README.md
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue