diff --git a/.gitignore b/.gitignore index d3872d7d..db8c5d7a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ Carthage/Build # `pod install` in .travis.yml # Pods/ +Podfile.lock # SwiftPM .build diff --git a/Example/Podfile.lock b/Example/Podfile.lock index d7840020..26923eb4 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -11,7 +11,7 @@ PODS: - SDWebImage (5.2.3): - SDWebImage/Core (= 5.2.3) - SDWebImage/Core (5.2.3) - - SDWebImageSwiftUI (0.4.1): + - SDWebImageSwiftUI (0.4.2): - SDWebImage (~> 5.1) - SDWebImageWebPCoder (0.2.5): - libwebp (~> 1.0) @@ -34,7 +34,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: libwebp: 057912d6d0abfb6357d8bb05c0ea470301f5d61e SDWebImage: 46a7f73228f84ce80990c786e4372cf4db5875ce - SDWebImageSwiftUI: 15eeed7470ba9cd64fa7e8dddd62e12df58d07f3 + SDWebImageSwiftUI: b91be76ecb0cdf74c18f6cd92aae8f19a9ded02d SDWebImageWebPCoder: 947093edd1349d820c40afbd9f42acb6cdecd987 PODFILE CHECKSUM: 3fb06a5173225e197f3a4bf2be7e5586a693257a diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index 9821bb17..4c2287c1 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -80,15 +80,34 @@ struct ContentView: View { NavigationLink(destination: DetailView(url: url, animated: self.animated)) { HStack { if self.animated { + #if os(macOS) || os(iOS) || os(tvOS) AnimatedImage(url: URL(string:url)) + .indicator(SDWebImageActivityIndicator.medium) + .transition(.fade) .resizable() .scaledToFit() .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) + #else + AnimatedImage(url: URL(string:url)) + .resizable() + .scaledToFit() + .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) + #endif } else { + #if os(macOS) || os(iOS) || os(tvOS) + WebImage(url: URL(string:url)) + .indicator(.activity) + .resizable() + .scaledToFit() + .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) + .animation(.easeInOut(duration: 0.5)) + .transition(.opacity) + #else WebImage(url: URL(string:url)) .resizable() .scaledToFit() .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) + #endif } Text((url as NSString).lastPathComponent) } diff --git a/Example/SDWebImageSwiftUIDemo/DetailView.swift b/Example/SDWebImageSwiftUIDemo/DetailView.swift index a6ba5537..c5e1cf92 100644 --- a/Example/SDWebImageSwiftUIDemo/DetailView.swift +++ b/Example/SDWebImageSwiftUIDemo/DetailView.swift @@ -12,17 +12,10 @@ import SDWebImageSwiftUI struct DetailView: View { let url: String let animated: Bool - @State var progress: CGFloat = 1 @State var isAnimating: Bool = true var body: some View { VStack { - HStack { - ProgressBar(value: $progress) - .foregroundColor(.blue) - .frame(maxHeight: 6) - } - Spacer() #if os(iOS) || os(tvOS) if animated { contentView() @@ -45,35 +38,40 @@ struct DetailView: View { contentView() } #endif - Spacer() } } func contentView() -> some View { HStack { if animated { + #if os(macOS) || os(iOS) || os(tvOS) + AnimatedImage(url: URL(string:url), options: [.progressiveLoad], isAnimating: $isAnimating) + .indicator(SDWebImageProgressIndicator.default) + .resizable() + .scaledToFit() + #else AnimatedImage(url: URL(string:url), options: [.progressiveLoad], isAnimating: $isAnimating) - .onProgress { receivedSize, expectedSize in - // SwiftUI engine itself ensure the main queue dispatch - if (expectedSize > 0) { - self.progress = CGFloat(receivedSize) / CGFloat(expectedSize) - } else { - self.progress = 1 - } - } .resizable() .scaledToFit() + #endif } else { + #if os(macOS) || os(iOS) || os(tvOS) WebImage(url: URL(string:url), options: [.progressiveLoad]) - .onProgress { receivedSize, expectedSize in - if (expectedSize > 0) { - self.progress = CGFloat(receivedSize) / CGFloat(expectedSize) - } else { - self.progress = 1 + .indicator(.progress) + .resizable() + .scaledToFit() + #else + WebImage(url: URL(string:url), options: [.progressiveLoad]) + .indicator( + Indicator { isAnimating, progress in + ProgressBar(value: progress) + .foregroundColor(.blue) + .frame(maxHeight: 6) } - } + ) .resizable() .scaledToFit() + #endif } } } diff --git a/Example/SDWebImageSwiftUIDemo/ProgressBar.swift b/Example/SDWebImageSwiftUIDemo/ProgressBar.swift index 44e586e2..14c231d1 100644 --- a/Example/SDWebImageSwiftUIDemo/ProgressBar.swift +++ b/Example/SDWebImageSwiftUIDemo/ProgressBar.swift @@ -14,15 +14,15 @@ public struct ProgressBar: View { public var body: some View { GeometryReader { geometry in - ZStack(alignment: .topLeading) { - Capsule() + ZStack(alignment: .leading) { + Rectangle() .frame(width: geometry.size.width) .opacity(0.3) Rectangle() .frame(width: geometry.size.width * self.value) + .opacity(0.6) } } - .clipShape(Capsule()) - .opacity(self.value < 1 ? 1 : 0) + .cornerRadius(2) } } diff --git a/README.md b/README.md index f780c67c..f087f55b 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,11 @@ let package = Package( ### Using `WebImage` to load network image -- [x] Supports the placeholder and detail options control for image loading as SDWebImage. -- [x] Supports the success/failure/progress changes event for custom handling. +- [x] Supports placeholder and detail options control for image loading as SDWebImage +- [x] Supports success/failure/progress changes event for custom handling +- [x] Supports indicator with activity/progress indicator and customization -Note: Unlike `UIImageView` in UIKit, SwiftUI's `Image` does not support animation. This `WebImage` using `Image` for internal implementation and supports static image format only. +Note: This `WebImage` using `Image` for internal implementation, which is the best compatible for SwiftUI layout and animation system. But it supports static image format only, because unlike `UIImageView` in UIKit, SwiftUI's `Image` does not support animation. ```swift var body: some View { @@ -72,6 +73,7 @@ var body: some View { .onSuccess { image, cacheType in // Success } + .indicator(.activity) // Activity Indicator .resizable() .scaledToFit() .frame(width: 300, height: 300, alignment: .center) @@ -88,6 +90,7 @@ var body: some View { .onFailure { error in // Error } + .transition(.fade) // Fade Transition .scaledToFit() // Data AnimatedImage(data: try! Data(contentsOf: URL(fileURLWithPath: "/tmp/foo.webp"))) @@ -101,9 +104,10 @@ var body: some View { - [x] Supports network image as well as local data and bundle image - [x] Supports animation control using the SwiftUI Binding -- [x] Supports advanced control like loop count, incremental load, buffer size. +- [x] Supports indicator and transition powered by SDWebImage and CoreAnimation +- [x] Supports advanced control like loop count, incremental load, buffer size -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. +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, if you need advanced customized layout and animation, you need CoreAnimation to help. Note: From v0.4.0, `AnimatedImage` supports watchOS as well. However, it's not backed by SDWebImage's [Animated ImageView](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) like iOS/tvOS/macOS. It use some tricks and hacks because of the limitation on current Apple's API. It also use Image/IO decoding system, which means it supports GIF and APNG format only, but not external format like WebP. diff --git a/SDWebImageSwiftUI.xcodeproj/project.pbxproj b/SDWebImageSwiftUI.xcodeproj/project.pbxproj index b539c52c..f0839e38 100644 --- a/SDWebImageSwiftUI.xcodeproj/project.pbxproj +++ b/SDWebImageSwiftUI.xcodeproj/project.pbxproj @@ -15,6 +15,18 @@ 324F61CC235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */; }; 324F61CD235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */; }; 324F61CE235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */; }; + 326B84822363350C0011BDFB /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B84812363350C0011BDFB /* Indicator.swift */; }; + 326B84832363350C0011BDFB /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B84812363350C0011BDFB /* Indicator.swift */; }; + 326B84842363350C0011BDFB /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B84812363350C0011BDFB /* Indicator.swift */; }; + 326B84852363350C0011BDFB /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B84812363350C0011BDFB /* Indicator.swift */; }; + 326B8487236335110011BDFB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B8486236335110011BDFB /* ActivityIndicator.swift */; }; + 326B8488236335110011BDFB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B8486236335110011BDFB /* ActivityIndicator.swift */; }; + 326B8489236335110011BDFB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B8486236335110011BDFB /* ActivityIndicator.swift */; }; + 326B848A236335110011BDFB /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B8486236335110011BDFB /* ActivityIndicator.swift */; }; + 326B848C236335400011BDFB /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B848B236335400011BDFB /* ProgressIndicator.swift */; }; + 326B848D236335400011BDFB /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B848B236335400011BDFB /* ProgressIndicator.swift */; }; + 326B848E236335400011BDFB /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B848B236335400011BDFB /* ProgressIndicator.swift */; }; + 326B848F236335400011BDFB /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B848B236335400011BDFB /* ProgressIndicator.swift */; }; 326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; }; 326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; }; 326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; }; @@ -99,6 +111,9 @@ /* Begin PBXFileReference section */ 324F61C5235E07EC003973B8 /* SDAnimatedImageInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDAnimatedImageInterface.h; sourceTree = ""; }; 324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDAnimatedImageInterface.m; sourceTree = ""; }; + 326B84812363350C0011BDFB /* Indicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Indicator.swift; sourceTree = ""; }; + 326B8486236335110011BDFB /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; + 326B848B236335400011BDFB /* ProgressIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = ""; }; 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewWrapper.swift; sourceTree = ""; }; 32C43DCC22FD540D00BE87F5 /* SDWebImageSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SDWebImageSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 32C43DDC22FD54C600BE87F5 /* ImageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageManager.swift; sourceTree = ""; }; @@ -161,6 +176,16 @@ path = ObjC; sourceTree = ""; }; + 326099472362E09E006EBB22 /* Indicator */ = { + isa = PBXGroup; + children = ( + 326B84812363350C0011BDFB /* Indicator.swift */, + 326B8486236335110011BDFB /* ActivityIndicator.swift */, + 326B848B236335400011BDFB /* ProgressIndicator.swift */, + ); + path = Indicator; + sourceTree = ""; + }; 32C43DC222FD540D00BE87F5 = { isa = PBXGroup; children = ( @@ -194,6 +219,7 @@ 32C43DDB22FD54C600BE87F5 /* Classes */ = { isa = PBXGroup; children = ( + 326099472362E09E006EBB22 /* Indicator */, 324F61C4235E07EC003973B8 /* ObjC */, 32C43DDC22FD54C600BE87F5 /* ImageManager.swift */, 32C43DDE22FD54C600BE87F5 /* WebImage.swift */, @@ -418,8 +444,11 @@ buildActionMask = 2147483647; files = ( 32C43E1722FD583700BE87F5 /* WebImage.swift in Sources */, + 326B848C236335400011BDFB /* ProgressIndicator.swift in Sources */, + 326B84822363350C0011BDFB /* Indicator.swift in Sources */, 32C43E3222FD5DE100BE87F5 /* SDWebImageSwiftUI.swift in Sources */, 326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, + 326B8487236335110011BDFB /* ActivityIndicator.swift in Sources */, 32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */, 32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */, 324F61CB235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */, @@ -431,8 +460,11 @@ buildActionMask = 2147483647; files = ( 32C43E1A22FD583700BE87F5 /* WebImage.swift in Sources */, + 326B848D236335400011BDFB /* ProgressIndicator.swift in Sources */, + 326B84832363350C0011BDFB /* Indicator.swift in Sources */, 32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, 326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, + 326B8488236335110011BDFB /* ActivityIndicator.swift in Sources */, 32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */, 32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */, 324F61CC235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */, @@ -444,8 +476,11 @@ buildActionMask = 2147483647; files = ( 32C43E1D22FD583800BE87F5 /* WebImage.swift in Sources */, + 326B848E236335400011BDFB /* ProgressIndicator.swift in Sources */, + 326B84842363350C0011BDFB /* Indicator.swift in Sources */, 32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, 326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, + 326B8489236335110011BDFB /* ActivityIndicator.swift in Sources */, 32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */, 32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */, 324F61CD235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */, @@ -457,8 +492,11 @@ buildActionMask = 2147483647; files = ( 32C43E2022FD583800BE87F5 /* WebImage.swift in Sources */, + 326B848F236335400011BDFB /* ProgressIndicator.swift in Sources */, + 326B84852363350C0011BDFB /* Indicator.swift in Sources */, 32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, 326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, + 326B848A236335110011BDFB /* ActivityIndicator.swift in Sources */, 32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */, 32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */, 324F61CE235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */, diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 3ed4aa34..9950150f 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -36,6 +36,11 @@ final class AnimatedImageConfiguration: ObservableObject { @Published var incrementalLoad: Bool? @Published var maxBufferSize: UInt? @Published var customLoopCount: Int? + #if os(macOS) || os(iOS) || os(tvOS) + // These configurations only useful for web image loading + @Published var indicator: SDWebImageIndicator? + @Published var transition: SDWebImageTransition? + #endif } // Convenient @@ -203,6 +208,10 @@ public struct AnimatedImage : PlatformViewRepresentable { #endif } else { if let url = url { + #if os(macOS) || os(iOS) || os(tvOS) + view.wrapped.sd_imageIndicator = imageConfiguration.indicator + view.wrapped.sd_imageTransition = imageConfiguration.transition + #endif loadImage(view, url: url) } } @@ -545,6 +554,28 @@ extension AnimatedImage { } } +#if os(macOS) || os(iOS) || os(tvOS) +// Web Image convenience +extension AnimatedImage { + + /// 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` + public func indicator(_ indicator: SDWebImageIndicator?) -> AnimatedImage { + imageConfiguration.indicator = indicator + return self + } + + /// Associate a transition when loading image with url + /// - Note: If you specify nil, do not do transition. Defautls to nil. + /// - Parameter transition: transition, see more in `SDWebImageTransition` + public func transition(_ transition: SDWebImageTransition?) -> AnimatedImage { + imageConfiguration.transition = transition + return self + } +} +#endif + #if DEBUG struct AnimatedImage_Previews : PreviewProvider { static var previews: some View { diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index 367d0d8e..755059c8 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -11,6 +11,8 @@ import SDWebImage class ImageManager : ObservableObject { @Published var image: PlatformImage? + @Published var isLoading: Bool = false + @Published var progress: CGFloat = 0 var manager = SDWebImageManager.shared weak var currentOperation: SDWebImageOperation? = nil @@ -32,8 +34,21 @@ class ImageManager : ObservableObject { if currentOperation != nil { return } + self.isLoading = true currentOperation = manager.loadImage(with: url, options: options, context: context, progress: { [weak self] (receivedSize, expectedSize, _) in - self?.progressBlock?(receivedSize, expectedSize) + guard let self = self else { + return + } + let progress: CGFloat + if (expectedSize > 0) { + progress = CGFloat(receivedSize) / CGFloat(expectedSize) + } else { + progress = 0 + } + DispatchQueue.main.async { + self.progress = progress + } + self.progressBlock?(receivedSize, expectedSize) }) { [weak self] (image, data, error, cacheType, finished, _) in guard let self = self else { return @@ -42,6 +57,8 @@ class ImageManager : ObservableObject { self.image = image } if finished { + self.isLoading = false + self.progress = 1 if let image = image { self.successBlock?(image, cacheType) } else { diff --git a/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift b/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift index 197d1c00..66dc64d4 100644 --- a/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift +++ b/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift @@ -11,7 +11,7 @@ import SDWebImage #if !os(watchOS) -// View Wrapper +/// Use wrapper to solve tne `UIImageView`/`NSImageView` frame size become image size issue (SwiftUI's Bug) public class AnimatedImageViewWrapper : PlatformView { var wrapped = SDAnimatedImageView() var interpolationQuality = CGInterpolationQuality.default @@ -54,4 +54,37 @@ public class AnimatedImageViewWrapper : PlatformView { } } +/// Use wrapper to solve the `UIProgressView`/`NSProgressIndicator` frame origin NaN crash (SwiftUI's bug) +public class ProgressIndicatorWrapper : PlatformView { + #if os(macOS) + var wrapped = NSProgressIndicator() + #else + var wrapped = UIProgressView(progressViewStyle: .default) + #endif + + #if os(macOS) + public override func layout() { + super.layout() + wrapped.frame = self.bounds + wrapped.setFrameOrigin(CGPoint(x: (self.bounds.width - wrapped.frame.width) / 2, y: (self.bounds.height - wrapped.frame.height) / 2)) + } + #else + public override func layoutSubviews() { + super.layoutSubviews() + wrapped.frame = self.bounds + wrapped.center = self.center + } + #endif + + public override init(frame frameRect: CGRect) { + super.init(frame: frameRect) + addSubview(wrapped) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + addSubview(wrapped) + } +} + #endif diff --git a/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift b/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift new file mode 100644 index 00000000..87f9881e --- /dev/null +++ b/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift @@ -0,0 +1,52 @@ +/* +* This file is part of the SDWebImage package. +* (c) DreamPiggy +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +import SwiftUI + +#if os(macOS) || os(iOS) || os(tvOS) +/// An activity indicator (system style) +public struct ActivityIndicator: PlatformViewRepresentable { + @Binding var isAnimating: Bool + + public init(_ isAnimating: Binding) { + self._isAnimating = isAnimating + } + + #if os(macOS) + public typealias NSViewType = NSProgressIndicator + #elseif os(iOS) || os(tvOS) + public typealias UIViewType = UIActivityIndicatorView + #endif + + #if os(iOS) || os(tvOS) + public func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { + let indicator = UIActivityIndicatorView(style: .medium) + indicator.hidesWhenStopped = true + return indicator + } + + public func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { + isAnimating ? uiView.startAnimating() : uiView.stopAnimating() + } + #endif + + #if os(macOS) + public func makeNSView(context: NSViewRepresentableContext) -> NSProgressIndicator { + let indicator = NSProgressIndicator() + indicator.style = .spinning + indicator.isDisplayedWhenStopped = false + return indicator + } + + public func updateNSView(_ nsView: NSProgressIndicator, context: NSViewRepresentableContext) { + isAnimating ? nsView.startAnimation(nil) : nsView.stopAnimation(nil) + } + + #endif +} +#endif diff --git a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift new file mode 100644 index 00000000..f52b37e0 --- /dev/null +++ b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift @@ -0,0 +1,44 @@ +/* +* This file is part of the SDWebImage package. +* (c) DreamPiggy +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +import Foundation +import SwiftUI + +/// A type to build the indicator +public struct Indicator { + var builder: (Binding, Binding) -> AnyView + + /// 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, _ progress: Binding) -> T) where T : View { + self.builder = { isAnimating, progress in + AnyView(builder(isAnimating, progress)) + } + } +} + +#if os(macOS) || os(iOS) || os(iOS) +extension Indicator { + /// Activity Indicator + public static var activity: Indicator { + Indicator { isAnimating, _ in + ActivityIndicator(isAnimating) + } + } + + /// Progress Indicator + public static var progress: Indicator { + Indicator { isAnimating, progress in + ProgressIndicator(isAnimating, progress: progress) + } + } +} +#endif diff --git a/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift b/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift new file mode 100644 index 00000000..81ac09ff --- /dev/null +++ b/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift @@ -0,0 +1,84 @@ +/* +* This file is part of the SDWebImage package. +* (c) DreamPiggy +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +import SwiftUI + +#if os(macOS) || os(iOS) || os(tvOS) +/// A progress bar indicator (system style) +public struct ProgressIndicator: PlatformViewRepresentable { + @Binding var isAnimating: Bool + @Binding var progress: CGFloat + + public init(_ isAnimating: Binding, progress: Binding) { + self._isAnimating = isAnimating + self._progress = progress + } + + #if os(macOS) + public typealias NSViewType = ProgressIndicatorWrapper + #elseif os(iOS) || os(tvOS) + public typealias UIViewType = ProgressIndicatorWrapper + #endif + + #if os(iOS) || os(tvOS) + public func makeUIView(context: UIViewRepresentableContext) -> ProgressIndicatorWrapper { + let uiView = ProgressIndicatorWrapper() + let view = uiView.wrapped + view.progressViewStyle = .default + return uiView + } + + public func updateUIView(_ uiView: ProgressIndicatorWrapper, context: UIViewRepresentableContext) { + let view = uiView.wrapped + if isAnimating { + view.setProgress(Float(progress), animated: true) + } else { + if progress == 0 { + view.isHidden = false + view.progress = 0 + } else { + view.isHidden = true + view.progress = 1 + } + } + } + #endif + + #if os(macOS) + public func makeNSView(context: NSViewRepresentableContext) -> ProgressIndicatorWrapper { + let nsView = ProgressIndicatorWrapper() + let view = nsView.wrapped + view.style = .bar + view.isDisplayedWhenStopped = false + view.controlSize = .small + return nsView + } + + public func updateNSView(_ nsView: ProgressIndicatorWrapper, context: NSViewRepresentableContext) { + let view = nsView.wrapped + if isAnimating { + view.isIndeterminate = false + view.doubleValue = Double(progress) * 100 + view.startAnimation(nil) + } else { + if progress == 0 { + view.isHidden = false + view.isIndeterminate = true + view.doubleValue = 0 + view.stopAnimation(nil) + } else { + view.isHidden = true + view.isIndeterminate = false + view.doubleValue = 100 + view.stopAnimation(nil) + } + } + } + #endif +} +#endif diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 75293fd8..87f887d1 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -16,8 +16,14 @@ public struct WebImage : View { var context: [SDWebImageContextOption : Any]? var configurations: [(Image) -> Image] = [] + var indicator: Indicator? @ObservedObject var imageManager: ImageManager + @State var progress: CGFloat = 0 + @State var isLoading: Bool = false + var isFinished: Bool { + !isLoading && (imageManager.image != nil) + } /// Create a web image with url, placeholder, custom options and context. /// - Parameter url: The image url @@ -46,7 +52,7 @@ public struct WebImage : View { // this can ensure we load the image, SDWebImage take care of the duplicated query self.imageManager.load() } - return configurations.reduce(image) { (previous, configuration) in + let view = configurations.reduce(image) { (previous, configuration) in configuration(previous) } .onAppear { @@ -57,6 +63,41 @@ public struct WebImage : View { .onDisappear { self.imageManager.cancel() } + // Convert Combine.Publisher to Binding + .onReceive(imageManager.$isLoading) { isLoading in + // only Apple Watch complain that "Modifying state during view update, this will cause undefined behavior." + // Use dispatch to workaround, Thanks Apple :) + #if os(watchOS) + DispatchQueue.main.async { + self.isLoading = isLoading + } + #else + self.isLoading = isLoading + #endif + } + .onReceive(imageManager.$progress) { progress in + #if os(watchOS) + DispatchQueue.main.async { + self.progress = progress + } + #else + self.progress = progress + #endif + } + if let indicator = indicator { + if isFinished { + return AnyView(view) + } else { + return AnyView( + ZStack { + view + indicator.builder($isLoading, $progress) + } + ) + } + } else { + return AnyView(view) + } } } @@ -128,6 +169,17 @@ extension WebImage { } } +extension WebImage { + + /// Associate a indicator when loading image with url + /// - Parameter indicator: The indicator type, see `Indicator` + public func indicator(_ indicator: Indicator) -> WebImage { + var result = self + result.indicator = indicator + return result + } +} + #if DEBUG struct WebImage_Previews : PreviewProvider { static var previews: some View {