From 547378465f5967d81c8f413ddad3a6511c198002 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 3 Mar 2020 21:06:00 +0800 Subject: [PATCH 1/2] Adopt the AnimatedImage with Indicator view modifier --- SDWebImageSwiftUI/Classes/AnimatedImage.swift | 45 +++++++++++++++++-- SDWebImageSwiftUI/Classes/ImageManager.swift | 6 +-- .../Classes/Indicator/Indicator.swift | 24 +++++++--- SDWebImageSwiftUI/Classes/WebImage.swift | 2 +- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 3528b47..2c42353 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -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(_ indicator: Indicator) -> 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(@ViewBuilder content: @escaping (_ isAnimating: Binding, _ progress: Binding) -> 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 { diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index 83d4f43..e54f145 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -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 diff --git a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift index 28d7612..0634e46 100644 --- a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift +++ b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift @@ -17,27 +17,39 @@ public struct Indicator 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, _ progress: Binding) -> 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 : ViewModifier where T : View { - @ObservedObject var imageManager: ImageManager +public struct IndicatorViewModifier : ViewModifier where T : View, V : IndicatorReportable { + /// The progress reporter + @ObservedObject var reporter: V + + /// The indicator var indicator: Indicator - 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) } } } diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 5aa0453..96d4a14 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -280,7 +280,7 @@ extension WebImage { /// Associate a indicator when loading image with url /// - Parameter indicator: The indicator type, see `Indicator` public func indicator(_ indicator: Indicator) -> 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 From e47153c65bffd3dfcc51f2f9f0d41b31d2fe226f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 3 Mar 2020 21:27:22 +0800 Subject: [PATCH 2/2] Add the readme about the AnimatedImage's API which share the same naming as SwiftUI View --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 3468b60..fff19a1 100644 --- a/README.md +++ b/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.