diff --git a/Example/Podfile.lock b/Example/Podfile.lock index d784002..26923eb 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 9821bb1..3ddd5de 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -81,11 +81,16 @@ struct ContentView: View { HStack { if self.animated { AnimatedImage(url: URL(string:url)) + .indicator(SDWebImageActivityIndicator.medium) + .transition(.fade) .resizable() .scaledToFit() .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) } else { WebImage(url: URL(string:url)) + .indicator { isAnimating, _ in + ActivityIndicator(isAnimating) + } .resizable() .scaledToFit() .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) diff --git a/SDWebImageSwiftUI.xcodeproj/project.pbxproj b/SDWebImageSwiftUI.xcodeproj/project.pbxproj index b539c52..9f427ce 100644 --- a/SDWebImageSwiftUI.xcodeproj/project.pbxproj +++ b/SDWebImageSwiftUI.xcodeproj/project.pbxproj @@ -15,6 +15,14 @@ 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 */; }; 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 +107,8 @@ /* 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 = ""; }; 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 +171,15 @@ path = ObjC; sourceTree = ""; }; + 326099472362E09E006EBB22 /* Indicator */ = { + isa = PBXGroup; + children = ( + 326B84812363350C0011BDFB /* Indicator.swift */, + 326B8486236335110011BDFB /* ActivityIndicator.swift */, + ); + path = Indicator; + sourceTree = ""; + }; 32C43DC222FD540D00BE87F5 = { isa = PBXGroup; children = ( @@ -194,6 +213,7 @@ 32C43DDB22FD54C600BE87F5 /* Classes */ = { isa = PBXGroup; children = ( + 326099472362E09E006EBB22 /* Indicator */, 324F61C4235E07EC003973B8 /* ObjC */, 32C43DDC22FD54C600BE87F5 /* ImageManager.swift */, 32C43DDE22FD54C600BE87F5 /* WebImage.swift */, @@ -418,8 +438,10 @@ buildActionMask = 2147483647; files = ( 32C43E1722FD583700BE87F5 /* WebImage.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 +453,10 @@ buildActionMask = 2147483647; files = ( 32C43E1A22FD583700BE87F5 /* WebImage.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 +468,10 @@ buildActionMask = 2147483647; files = ( 32C43E1D22FD583800BE87F5 /* WebImage.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 +483,10 @@ buildActionMask = 2147483647; files = ( 32C43E2022FD583800BE87F5 /* WebImage.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/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index 367d0d8..3f63485 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 + } + self.progressBlock?(receivedSize, expectedSize) + let progress: CGFloat + if (expectedSize > 0) { + progress = CGFloat(receivedSize) / CGFloat(expectedSize) + } else { + progress = 0 + } + DispatchQueue.main.async { + self.progress = progress + } }) { [weak self] (image, data, error, cacheType, finished, _) in guard let self = self else { return @@ -42,6 +57,7 @@ class ImageManager : ObservableObject { self.image = image } if finished { + self.isLoading = false if let image = image { self.successBlock?(image, cacheType) } else { diff --git a/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift b/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift new file mode 100644 index 0000000..2ad5546 --- /dev/null +++ b/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift @@ -0,0 +1,50 @@ +/* +* 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 Swift +import SwiftUI + +public struct ActivityIndicator: PlatformViewRepresentable { + @Binding var isAnimating: Bool + + public init(_ isAnimating: Binding = .constant(true)) { + 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 +} diff --git a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift new file mode 100644 index 0000000..7694459 --- /dev/null +++ b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift @@ -0,0 +1,23 @@ +/* +* 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 + +public struct Indicator : View { + var builder: (Binding, Binding) -> AnyView + public typealias Body = Never + public var body: Never { + fatalError() + } + public init(@ViewBuilder builder: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) where T : View { + self.builder = { isAnimating, progress in + AnyView(builder(isAnimating, progress)) + } + } +} diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 75293fd..34c9f77 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -16,8 +16,11 @@ 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 /// Create a web image with url, placeholder, custom options and context. /// - Parameter url: The image url @@ -46,7 +49,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 +60,19 @@ public struct WebImage : View { .onDisappear { self.imageManager.cancel() } + // Convert Combine.Publisher to Binding, I think this need a better API from Apple :) + .onReceive(imageManager.$isLoading) { self.isLoading = $0 } + .onReceive(imageManager.$progress) { self.progress = $0 } + if let indicator = indicator { + return AnyView( + ZStack { + view + indicator.builder($isLoading, $progress) + } + ) + } else { + return AnyView(view) + } } } @@ -128,6 +144,19 @@ extension WebImage { } } +extension WebImage { + + /// Associate a indicator when loading image with url + /// - Parameter builder: builder description + /// - Parameter isAnimating: A Binding to control the animation. If image is loading, the value is true, else false. + /// - Parameter progress: A Binding to control the progress during loading. If no progress can be reported, the value is 0. + public func indicator(_ builder: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) -> WebImage where T : View { + var result = self + result.indicator = Indicator(builder: builder) + return result + } +} + #if DEBUG struct WebImage_Previews : PreviewProvider { static var previews: some View {