From cbe282b69c4e9216c7cf177dcb16ec74fb381a97 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 25 Oct 2019 14:54:09 +0800 Subject: [PATCH 1/9] Add support for AnimatedImage to load indicator and transition, by using SDWebImage's already supported features --- SDWebImageSwiftUI/Classes/AnimatedImage.swift | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 3ed4aa34..09eecbc6 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -36,6 +36,9 @@ final class AnimatedImageConfiguration: ObservableObject { @Published var incrementalLoad: Bool? @Published var maxBufferSize: UInt? @Published var customLoopCount: Int? + // These configurations only useful for web image loading + @Published var indicator: SDWebImageIndicator? + @Published var transition: SDWebImageTransition? } // Convenient @@ -203,6 +206,8 @@ public struct AnimatedImage : PlatformViewRepresentable { #endif } else { if let url = url { + view.wrapped.sd_imageIndicator = imageConfiguration.indicator + view.wrapped.sd_imageTransition = imageConfiguration.transition loadImage(view, url: url) } } @@ -545,6 +550,26 @@ extension AnimatedImage { } } +// 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 + } +} + #if DEBUG struct AnimatedImage_Previews : PreviewProvider { static var previews: some View { From 6a2eb0264adf7498fd01fc9886d4184cbaaea98f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 25 Oct 2019 21:48:03 +0800 Subject: [PATCH 2/9] Add support for WebImage to use indicator. Using protocol and struct based solution, and with SwiftUI Binding for isAnimating and progress --- Example/Podfile.lock | 4 +- .../SDWebImageSwiftUIDemo/ContentView.swift | 5 ++ SDWebImageSwiftUI.xcodeproj/project.pbxproj | 28 +++++++++++ SDWebImageSwiftUI/Classes/ImageManager.swift | 18 ++++++- .../Classes/Indicator/ActivityIndicator.swift | 50 +++++++++++++++++++ .../Classes/Indicator/Indicator.swift | 23 +++++++++ SDWebImageSwiftUI/Classes/WebImage.swift | 31 +++++++++++- 7 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift create mode 100644 SDWebImageSwiftUI/Classes/Indicator/Indicator.swift 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..3ddd5dee 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 b539c52c..9f427ce6 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 367d0d8e..3f634858 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 00000000..2ad55460 --- /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 00000000..7694459a --- /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 75293fd8..34c9f77a 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 { From 40c56445699c6c0a55ec363e249132976207993b Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 25 Oct 2019 22:55:19 +0800 Subject: [PATCH 3/9] Add activity indicator implementation as well, using UIProgressView/NSProgressIndicator --- .../SDWebImageSwiftUIDemo/DetailView.swift | 8 +- SDWebImageSwiftUI.xcodeproj/project.pbxproj | 10 +++ SDWebImageSwiftUI/Classes/ImageManager.swift | 3 +- .../Classes/ImageViewWrapper.swift | 34 +++++++- .../Classes/Indicator/ActivityIndicator.swift | 4 +- .../Classes/Indicator/Indicator.swift | 1 + .../Classes/Indicator/ProgressIndicator.swift | 83 +++++++++++++++++++ 7 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift diff --git a/Example/SDWebImageSwiftUIDemo/DetailView.swift b/Example/SDWebImageSwiftUIDemo/DetailView.swift index a6ba5537..ac5f17be 100644 --- a/Example/SDWebImageSwiftUIDemo/DetailView.swift +++ b/Example/SDWebImageSwiftUIDemo/DetailView.swift @@ -65,12 +65,8 @@ struct DetailView: View { .scaledToFit() } else { 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 { isAnimating, progress in + ProgressIndicator(isAnimating, progress: progress) } .resizable() .scaledToFit() diff --git a/SDWebImageSwiftUI.xcodeproj/project.pbxproj b/SDWebImageSwiftUI.xcodeproj/project.pbxproj index 9f427ce6..f0839e38 100644 --- a/SDWebImageSwiftUI.xcodeproj/project.pbxproj +++ b/SDWebImageSwiftUI.xcodeproj/project.pbxproj @@ -23,6 +23,10 @@ 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 */; }; @@ -109,6 +113,7 @@ 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 = ""; }; @@ -176,6 +181,7 @@ children = ( 326B84812363350C0011BDFB /* Indicator.swift */, 326B8486236335110011BDFB /* ActivityIndicator.swift */, + 326B848B236335400011BDFB /* ProgressIndicator.swift */, ); path = Indicator; sourceTree = ""; @@ -438,6 +444,7 @@ 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 */, @@ -453,6 +460,7 @@ 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 */, @@ -468,6 +476,7 @@ 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 */, @@ -483,6 +492,7 @@ 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 */, diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index 3f634858..755059c8 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -39,7 +39,6 @@ class ImageManager : ObservableObject { guard let self = self else { return } - self.progressBlock?(receivedSize, expectedSize) let progress: CGFloat if (expectedSize > 0) { progress = CGFloat(receivedSize) / CGFloat(expectedSize) @@ -49,6 +48,7 @@ class ImageManager : ObservableObject { DispatchQueue.main.async { self.progress = progress } + self.progressBlock?(receivedSize, expectedSize) }) { [weak self] (image, data, error, cacheType, finished, _) in guard let self = self else { return @@ -58,6 +58,7 @@ class ImageManager : ObservableObject { } 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..4e5da9fe 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,36 @@ 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 + } + #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 index 2ad55460..2305972c 100644 --- a/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift +++ b/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift @@ -6,13 +6,13 @@ * file that was distributed with this source code. */ -import Swift import SwiftUI +/// An activity indicator (system style) public struct ActivityIndicator: PlatformViewRepresentable { @Binding var isAnimating: Bool - public init(_ isAnimating: Binding = .constant(true)) { + public init(_ isAnimating: Binding) { self._isAnimating = isAnimating } diff --git a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift index 7694459a..7e588370 100644 --- a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift +++ b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI +/// A container view to hold the indicator builder public struct Indicator : View { var builder: (Binding, Binding) -> AnyView public typealias Body = Never diff --git a/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift b/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift new file mode 100644 index 00000000..b01d0d25 --- /dev/null +++ b/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift @@ -0,0 +1,83 @@ +/* +* 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 + +/// 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 +} From 47fc670e969f94d47b073a5e21d1c3074310c85c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 25 Oct 2019 23:59:55 +0800 Subject: [PATCH 4/9] Fix the watchOS platform compile issues --- Example/SDWebImageSwiftUIDemo/ContentView.swift | 14 ++++++++++++++ Example/SDWebImageSwiftUIDemo/DetailView.swift | 14 ++++++++++++++ SDWebImageSwiftUI/Classes/AnimatedImage.swift | 6 ++++++ SDWebImageSwiftUI/Classes/ImageViewWrapper.swift | 1 + .../Classes/Indicator/ActivityIndicator.swift | 2 ++ .../Classes/Indicator/ProgressIndicator.swift | 3 ++- 6 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index 3ddd5dee..a4181722 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -80,13 +80,21 @@ 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 { isAnimating, _ in ActivityIndicator(isAnimating) @@ -94,6 +102,12 @@ struct ContentView: View { .resizable() .scaledToFit() .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) + #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 ac5f17be..353508a4 100644 --- a/Example/SDWebImageSwiftUIDemo/DetailView.swift +++ b/Example/SDWebImageSwiftUIDemo/DetailView.swift @@ -64,12 +64,26 @@ struct DetailView: View { .resizable() .scaledToFit() } else { + #if os(macOS) || os(iOS) || os(tvOS) WebImage(url: URL(string:url), options: [.progressiveLoad]) .indicator { isAnimating, progress in ProgressIndicator(isAnimating, progress: progress) } .resizable() .scaledToFit() + #else + WebImage(url: URL(string:url), options: [.progressiveLoad]) + .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 } } } diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 09eecbc6..9950150f 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -36,9 +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 @@ -206,8 +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) } } @@ -550,6 +554,7 @@ extension AnimatedImage { } } +#if os(macOS) || os(iOS) || os(tvOS) // Web Image convenience extension AnimatedImage { @@ -569,6 +574,7 @@ extension AnimatedImage { return self } } +#endif #if DEBUG struct AnimatedImage_Previews : PreviewProvider { diff --git a/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift b/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift index 4e5da9fe..66dc64d4 100644 --- a/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift +++ b/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift @@ -66,6 +66,7 @@ public class ProgressIndicatorWrapper : PlatformView { 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() { diff --git a/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift b/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift index 2305972c..87f9881e 100644 --- a/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift +++ b/SDWebImageSwiftUI/Classes/Indicator/ActivityIndicator.swift @@ -8,6 +8,7 @@ import SwiftUI +#if os(macOS) || os(iOS) || os(tvOS) /// An activity indicator (system style) public struct ActivityIndicator: PlatformViewRepresentable { @Binding var isAnimating: Bool @@ -48,3 +49,4 @@ public struct ActivityIndicator: PlatformViewRepresentable { #endif } +#endif diff --git a/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift b/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift index b01d0d25..81ac09ff 100644 --- a/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift +++ b/SDWebImageSwiftUI/Classes/Indicator/ProgressIndicator.swift @@ -8,6 +8,7 @@ import SwiftUI +#if os(macOS) || os(iOS) || os(tvOS) /// A progress bar indicator (system style) public struct ProgressIndicator: PlatformViewRepresentable { @Binding var isAnimating: Bool @@ -78,6 +79,6 @@ public struct ProgressIndicator: PlatformViewRepresentable { } } } - #endif } +#endif From 59d233d6b1f6e6f7a6c18946391bddf7e1d0cb8c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 26 Oct 2019 00:12:59 +0800 Subject: [PATCH 5/9] Try to workaround Apple Watch's issues about that runtime warning, other platform does not have this issue --- SDWebImageSwiftUI/Classes/WebImage.swift | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 34c9f77a..10d09b94 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -60,9 +60,27 @@ 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 } + // 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 { return AnyView( ZStack { From 057eb272d2954c9ae5ef262e75a68454b0af5f0f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 26 Oct 2019 00:26:03 +0800 Subject: [PATCH 6/9] Add gitignore to that Podfile.lock --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 5bcdaf697dabafc61d5875858c79b5cf22bb3fc6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 26 Oct 2019 01:26:47 +0800 Subject: [PATCH 7/9] Fix the issue when image load success, some bad-written indicator will still exist. Now we remove all the view when finished. Fix the example --- .../SDWebImageSwiftUIDemo/DetailView.swift | 34 ++++++------------- .../SDWebImageSwiftUIDemo/ProgressBar.swift | 10 +++--- SDWebImageSwiftUI/Classes/WebImage.swift | 21 ++++++++---- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/Example/SDWebImageSwiftUIDemo/DetailView.swift b/Example/SDWebImageSwiftUIDemo/DetailView.swift index 353508a4..0b9b5a5a 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,24 +38,22 @@ 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) - .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 - } - } + .indicator(SDWebImageProgressIndicator.default) .resizable() .scaledToFit() + #else + AnimatedImage(url: URL(string:url), options: [.progressiveLoad], isAnimating: $isAnimating) + .resizable() + .scaledToFit() + #endif } else { #if os(macOS) || os(iOS) || os(tvOS) WebImage(url: URL(string:url), options: [.progressiveLoad]) @@ -73,13 +64,10 @@ struct DetailView: View { .scaledToFit() #else WebImage(url: URL(string:url), options: [.progressiveLoad]) - .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 - } + .indicator { isAnimating, progress in + ProgressBar(value: progress) + .foregroundColor(.blue) + .frame(maxHeight: 6) } .resizable() .scaledToFit() diff --git a/Example/SDWebImageSwiftUIDemo/ProgressBar.swift b/Example/SDWebImageSwiftUIDemo/ProgressBar.swift index 44e586e2..d8ed095d 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() - .frame(width: geometry.size.width) + 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/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 10d09b94..423d3a40 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -21,6 +21,9 @@ public struct WebImage : View { @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 @@ -82,12 +85,16 @@ public struct WebImage : View { #endif } if let indicator = indicator { - return AnyView( - ZStack { - view - indicator.builder($isLoading, $progress) - } - ) + if isFinished { + return AnyView(view) + } else { + return AnyView( + ZStack { + view + indicator.builder($isLoading, $progress) + } + ) + } } else { return AnyView(view) } @@ -166,7 +173,7 @@ 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 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. public func indicator(_ builder: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) -> WebImage where T : View { var result = self From 896627d3816bf25a834208545a2b5121352f0ebf Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 26 Oct 2019 03:47:28 +0800 Subject: [PATCH 8/9] Update the readme with indicator and transition --- Example/SDWebImageSwiftUIDemo/ContentView.swift | 2 ++ README.md | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index a4181722..df566adf 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -102,6 +102,8 @@ struct ContentView: View { .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() diff --git a/README.md b/README.md index f780c67c..0e333c64 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 the placeholder and detail options control for image loading as SDWebImage +- [x] Supports the success/failure/progress changes event for custom handling +- [x] Supports the 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,9 @@ var body: some View { .onSuccess { image, cacheType in // Success } + .indicator { isAnimating, _ in + ActivityIndicator(isAnimating) // Activity Indicator + } .resizable() .scaledToFit() .frame(width: 300, height: 300, alignment: .center) @@ -88,6 +92,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 +106,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. From 63b6da1d5e636ef3f6cd95d976f8938f6b044b0c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 26 Oct 2019 04:26:07 +0800 Subject: [PATCH 9/9] Changing the indicator API to use the dot syntax, instead of that closure syntax, which is more convenient and swifty --- .../SDWebImageSwiftUIDemo/ContentView.swift | 4 +-- .../SDWebImageSwiftUIDemo/DetailView.swift | 16 +++++----- .../SDWebImageSwiftUIDemo/ProgressBar.swift | 2 +- README.md | 10 +++--- .../Classes/Indicator/Indicator.swift | 32 +++++++++++++++---- SDWebImageSwiftUI/Classes/WebImage.swift | 8 ++--- 6 files changed, 43 insertions(+), 29 deletions(-) diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index df566adf..4c2287c1 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -96,9 +96,7 @@ struct ContentView: View { } else { #if os(macOS) || os(iOS) || os(tvOS) WebImage(url: URL(string:url)) - .indicator { isAnimating, _ in - ActivityIndicator(isAnimating) - } + .indicator(.activity) .resizable() .scaledToFit() .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) diff --git a/Example/SDWebImageSwiftUIDemo/DetailView.swift b/Example/SDWebImageSwiftUIDemo/DetailView.swift index 0b9b5a5a..c5e1cf92 100644 --- a/Example/SDWebImageSwiftUIDemo/DetailView.swift +++ b/Example/SDWebImageSwiftUIDemo/DetailView.swift @@ -57,18 +57,18 @@ struct DetailView: View { } else { #if os(macOS) || os(iOS) || os(tvOS) WebImage(url: URL(string:url), options: [.progressiveLoad]) - .indicator { isAnimating, progress in - ProgressIndicator(isAnimating, progress: progress) - } + .indicator(.progress) .resizable() .scaledToFit() #else WebImage(url: URL(string:url), options: [.progressiveLoad]) - .indicator { isAnimating, progress in - ProgressBar(value: progress) - .foregroundColor(.blue) - .frame(maxHeight: 6) - } + .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 d8ed095d..14c231d1 100644 --- a/Example/SDWebImageSwiftUIDemo/ProgressBar.swift +++ b/Example/SDWebImageSwiftUIDemo/ProgressBar.swift @@ -16,7 +16,7 @@ public struct ProgressBar: View { GeometryReader { geometry in ZStack(alignment: .leading) { Rectangle() - .frame(width: geometry.size.width) + .frame(width: geometry.size.width) .opacity(0.3) Rectangle() .frame(width: geometry.size.width * self.value) diff --git a/README.md b/README.md index 0e333c64..f087f55b 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,9 @@ 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 the indicator with activity/progress indicator and customization +- [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: 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. @@ -73,9 +73,7 @@ var body: some View { .onSuccess { image, cacheType in // Success } - .indicator { isAnimating, _ in - ActivityIndicator(isAnimating) // Activity Indicator - } + .indicator(.activity) // Activity Indicator .resizable() .scaledToFit() .frame(width: 300, height: 300, alignment: .center) diff --git a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift index 7e588370..f52b37e0 100644 --- a/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift +++ b/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift @@ -9,16 +9,36 @@ import Foundation import SwiftUI -/// A container view to hold the indicator builder -public struct Indicator : View { +/// A type to build the indicator +public struct Indicator { var builder: (Binding, Binding) -> AnyView - public typealias Body = Never - public var body: Never { - fatalError() - } + + /// 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/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 423d3a40..87f887d1 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -172,12 +172,10 @@ 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 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. - public func indicator(_ builder: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) -> WebImage where T : View { + /// - Parameter indicator: The indicator type, see `Indicator` + public func indicator(_ indicator: Indicator) -> WebImage { var result = self - result.indicator = Indicator(builder: builder) + result.indicator = indicator return result } }