diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index e75e6afb..6dc50ad5 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -96,22 +96,19 @@ struct ContentView: View { HStack { if self.animated { #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) - AnimatedImage(url: URL(string:url), isAnimating: .constant(true)) + AnimatedImage(url: URL(string:url)) .onViewUpdate { view, context in #if os(macOS) view.toolTip = url #endif } - .indicator(SDWebImageActivityIndicator.medium) - /** - .placeholder(UIImage(systemName: "photo")) - */ + .indicator(.activity) .transition(.fade) .resizable() .scaledToFit() .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) #else - WebImage(url: URL(string:url), isAnimating: self.$animated) + WebImage(url: URL(string:url)) .resizable() .indicator(.activity) .transition(.fade(duration: 0.5)) @@ -119,13 +116,8 @@ struct ContentView: View { .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) #endif } else { - WebImage(url: URL(string:url), isAnimating: .constant(true)) + WebImage(url: URL(string:url)) .resizable() - /** - .placeholder { - Image(systemName: "photo") - } - */ .indicator(.activity) .transition(.fade(duration: 0.5)) .scaledToFit() diff --git a/Example/SDWebImageSwiftUIDemo/DetailView.swift b/Example/SDWebImageSwiftUIDemo/DetailView.swift index 4406f553..35621a76 100644 --- a/Example/SDWebImageSwiftUIDemo/DetailView.swift +++ b/Example/SDWebImageSwiftUIDemo/DetailView.swift @@ -95,10 +95,9 @@ struct DetailView: View { HStack { if animated { #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) - AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) + AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating, placeholderImage: .wifiExclamationmark) + .indicator(.progress) .resizable() - .placeholder(.wifiExclamationmark) - .indicator(SDWebImageProgressIndicator.default) .scaledToFit() #else WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in diff --git a/README.md b/README.md index e749b607..0fef4b1f 100644 --- a/README.md +++ b/README.md @@ -128,18 +128,16 @@ github "SDWebImage/SDWebImageSwiftUI" ```swift var body: some View { - WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic")) + WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic")) { image in + image.resizable() // Control layout like SwiftUI.AsyncImage, you must use this modifier or the view will use the image bitmap size + } placeholder: { + Rectangle().foregroundColor(.gray) + } // Supports options and context, like `.delayPlaceholder` to show placeholder only when error .onSuccess { image, data, cacheType in // Success // Note: Data exist only when queried from disk cache or network. Use `.queryMemoryData` if you really need data } - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder(Image(systemName: "photo")) // Placeholder Image - // Supports ViewBuilder as well - .placeholder { - Rectangle().foregroundColor(.gray) - } .indicator(.activity) // Activity Indicator .transition(.fade(duration: 0.5)) // Fade Transition with duration .scaledToFit() @@ -194,21 +192,21 @@ WebImage(url: url) ```swift var body: some View { Group { - AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif")) + AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"), placeholderImage: .init(systemName: "photo")) // Placeholder Image // Supports options and context, like `.progressiveLoad` for progressive animation loading .onFailure { error in // Error } .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder(UIImage(systemName: "photo")) // Placeholder Image - // Supports ViewBuilder as well - .placeholder { - Circle().foregroundColor(.gray) - } - .indicator(SDWebImageActivityIndicator.medium) // Activity Indicator + .indicator(.activity) // Activity Indicator .transition(.fade) // Fade Transition .scaledToFit() // Attention to call it on AnimatedImage, but not `some View` after View Modifier (Swift Protocol Extension method is static dispatched) + // Supports SwiftUI ViewBuilder placeholder as well + AnimatedImage(url: url) { + Circle().foregroundColor(.gray) + } + // Data AnimatedImage(data: try! Data(contentsOf: URL(fileURLWithPath: "/tmp/foo.webp"))) .customLoopCount(1) // Custom loop count diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index ef187e20..0e2f5c03 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -31,6 +31,12 @@ final class AnimatedImageModel : ObservableObject { @Published var url: URL? @Published var webOptions: SDWebImageOptions = [] @Published var webContext: [SDWebImageContextOption : Any]? = nil + @Published var placeholderImage: PlatformImage? + @Published var placeholderView: PlatformView? { + didSet { + oldValue?.removeFromSuperview() + } + } /// Name image @Published var name: String? @Published var bundle: Bundle? @@ -90,12 +96,6 @@ final class AnimatedImageConfiguration: ObservableObject { // These configurations only useful for web image loading var indicator: SDWebImageIndicator? var transition: SDWebImageTransition? - var placeholder: PlatformImage? - var placeholderView: PlatformView? { - didSet { - oldValue?.removeFromSuperview() - } - } } /// A Image View type to load image from url, data or bundle. Supports animated and static image format. @@ -115,13 +115,19 @@ public struct AnimatedImage : PlatformViewRepresentable { /// True to start animation, false to stop animation. @Binding public var isAnimating: Bool - /// Create an animated image with url, placeholder, custom options and context. + /// Create an animated image with url, placeholder, custom options and context, including animation control binding. /// - Parameter url: The image url /// - Parameter placeholder: The placeholder image to show during loading /// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values. /// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. - public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) { - self.init(url: url, options: options, context: context, isAnimating: .constant(true)) + /// - Parameter isAnimating: The binding for animation control + public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding = .constant(true), placeholderImage: PlatformImage? = nil) { + let imageModel = AnimatedImageModel() + imageModel.url = url + imageModel.webOptions = options + imageModel.webContext = context + imageModel.placeholderImage = placeholderImage + self.init(imageModel: imageModel, isAnimating: isAnimating) } /// Create an animated image with url, placeholder, custom options and context, including animation control binding. @@ -130,46 +136,37 @@ public struct AnimatedImage : PlatformViewRepresentable { /// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values. /// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. /// - Parameter isAnimating: The binding for animation control - public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding) { + public init(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil, isAnimating: Binding = .constant(true), @ViewBuilder placeholder: @escaping () -> T) where T : View { let imageModel = AnimatedImageModel() imageModel.url = url imageModel.webOptions = options imageModel.webContext = context + #if os(macOS) + let hostingView = NSHostingView(rootView: placeholder()) + #else + let hostingView = _UIHostingView(rootView: placeholder()) + #endif + imageModel.placeholderView = hostingView self.init(imageModel: imageModel, isAnimating: isAnimating) } - /// Create an animated image with name and bundle. - /// - Note: Asset Catalog is not supported. - /// - Parameter name: The image name - /// - Parameter bundle: The bundle contains image - public init(name: String, bundle: Bundle? = nil) { - self.init(name: name, bundle: bundle, isAnimating: .constant(true)) - } - /// Create an animated image with name and bundle, including animation control binding. /// - Note: Asset Catalog is not supported. /// - Parameter name: The image name /// - Parameter bundle: The bundle contains image /// - Parameter isAnimating: The binding for animation control - public init(name: String, bundle: Bundle? = nil, isAnimating: Binding) { + public init(name: String, bundle: Bundle? = nil, isAnimating: Binding = .constant(true)) { let imageModel = AnimatedImageModel() imageModel.name = name imageModel.bundle = bundle self.init(imageModel: imageModel, isAnimating: isAnimating) } - /// Create an animated image with data and scale. - /// - Parameter data: The image data - /// - Parameter scale: The scale factor - public init(data: Data, scale: CGFloat = 1) { - self.init(data: data, scale: scale, isAnimating: .constant(true)) - } - /// Create an animated image with data and scale, including animation control binding. /// - Parameter data: The image data /// - Parameter scale: The scale factor /// - Parameter isAnimating: The binding for animation control - public init(data: Data, scale: CGFloat = 1, isAnimating: Binding) { + public init(data: Data, scale: CGFloat = 1, isAnimating: Binding = .constant(true)) { let imageModel = AnimatedImageModel() imageModel.data = data imageModel.scale = scale @@ -222,7 +219,7 @@ public struct AnimatedImage : PlatformViewRepresentable { func setupIndicator(_ view: AnimatedImageViewWrapper, context: Context) { view.wrapped.sd_imageIndicator = imageConfiguration.indicator view.wrapped.sd_imageTransition = imageConfiguration.transition - if let placeholderView = imageConfiguration.placeholderView { + if let placeholderView = imageModel.placeholderView { placeholderView.removeFromSuperview() placeholderView.isHidden = true // Placeholder View should below the Indicator View @@ -243,13 +240,13 @@ public struct AnimatedImage : PlatformViewRepresentable { context.coordinator.imageLoading.isLoading = true let webOptions = imageModel.webOptions if webOptions.contains(.delayPlaceholder) { - self.imageConfiguration.placeholderView?.isHidden = true + self.imageModel.placeholderView?.isHidden = true } else { - self.imageConfiguration.placeholderView?.isHidden = false + self.imageModel.placeholderView?.isHidden = false } var webContext = imageModel.webContext ?? [:] webContext[.animatedImageClass] = SDAnimatedImage.self - view.wrapped.sd_internalSetImage(with: imageModel.url, placeholderImage: imageConfiguration.placeholder, options: webOptions, context: webContext, setImageBlock: nil, progress: { (receivedSize, expectedSize, _) in + view.wrapped.sd_internalSetImage(with: imageModel.url, placeholderImage: imageModel.placeholderImage, options: webOptions, context: webContext, setImageBlock: nil, progress: { (receivedSize, expectedSize, _) in let progress: Double if (expectedSize > 0) { progress = Double(receivedSize) / Double(expectedSize) @@ -265,10 +262,10 @@ public struct AnimatedImage : PlatformViewRepresentable { context.coordinator.imageLoading.isLoading = false context.coordinator.imageLoading.progress = 1 if let image = image { - self.imageConfiguration.placeholderView?.isHidden = true + self.imageModel.placeholderView?.isHidden = true self.imageHandler.successBlock?(image, data, cacheType) } else { - self.imageConfiguration.placeholderView?.isHidden = false + self.imageModel.placeholderView?.isHidden = false self.imageHandler.failureBlock?(error ?? NSError()) } } @@ -780,30 +777,19 @@ extension AnimatedImage { } } +// Convenient indicator dot syntax +extension SDWebImageIndicator where Self == SDWebImageActivityIndicator { + public static var activity: Self { Self() } +} + +extension SDWebImageIndicator where Self == SDWebImageProgressIndicator { + public static var progress: Self { Self() } +} + // Web Image convenience, based on UIKit/AppKit API @available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) extension AnimatedImage { - /// Associate a placeholder when loading image with url - /// - Parameter content: A view that describes the placeholder. - /// - note: The differences between this and placeholder image, it's that placeholder image replace the image for image view, but this modify the View Hierarchy to overlay the placeholder hosting view - public func placeholder(@ViewBuilder content: () -> T) -> AnimatedImage where T : View { - #if os(macOS) - let hostingView = NSHostingView(rootView: content()) - #else - let hostingView = _UIHostingView(rootView: content()) - #endif - self.imageConfiguration.placeholderView = hostingView - return self - } - - /// Associate a placeholder image when loading image with url - /// - Parameter content: A view that describes the placeholder. - public func placeholder(_ image: PlatformImage?) -> AnimatedImage { - self.imageConfiguration.placeholder = image - return self - } - /// 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` @@ -821,23 +807,6 @@ extension AnimatedImage { } } -// Indicator -@available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) -extension AnimatedImage { - - /// Associate a indicator when loading image with url - /// - Parameter indicator: The indicator type, see `Indicator` - public func indicator(_ indicator: Indicator) -> some View where T : View { - return self.modifier(IndicatorViewModifier(status: indicatorStatus, indicator: indicator)) - } - - /// Associate a indicator when loading image with url, convenient method with block - /// - Parameter content: A view that describes the indicator. - public func indicator(@ViewBuilder content: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) -> some View where T : View { - return indicator(Indicator(content: content)) - } -} - #if DEBUG @available(iOS 14.0, OSX 11.0, tvOS 14.0, watchOS 7.0, *) struct AnimatedImage_Previews : PreviewProvider { diff --git a/Tests/AnimatedImageTests.swift b/Tests/AnimatedImageTests.swift index 29472033..613539e8 100644 --- a/Tests/AnimatedImageTests.swift +++ b/Tests/AnimatedImageTests.swift @@ -142,7 +142,9 @@ class AnimatedImageTests: XCTestCase { func testAnimatedImageModifier() throws { let expectation = self.expectation(description: "WebImage modifier") let imageUrl = URL(string: "https://assets.sbnation.com/assets/2512203/dogflops.gif") - let imageView = AnimatedImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) + let imageView = AnimatedImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) { + Circle() + } let introspectView = imageView .onSuccess { _, _, _ in expectation.fulfill() @@ -161,11 +163,7 @@ class AnimatedImageTests: XCTestCase { XCTAssert(view.isKind(of: SDAnimatedImageView.self)) XCTAssertEqual(context.coordinator.userInfo?["foo"] as? String, "bar") } - .placeholder(PlatformImage()) - .placeholder { - Circle() - } - .indicator(SDWebImageActivityIndicator.medium) + .indicator(.activity) // Image .resizable() .renderingMode(.original) diff --git a/Tests/WebImageTests.swift b/Tests/WebImageTests.swift index d51efdbc..36090720 100644 --- a/Tests/WebImageTests.swift +++ b/Tests/WebImageTests.swift @@ -73,7 +73,11 @@ class WebImageTests: XCTestCase { func testWebImageModifier() throws { let expectation = self.expectation(description: "WebImage modifier") let imageUrl = URL(string: "https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg") - let imageView = WebImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) + let imageView = WebImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) { image in + image.resizable() + } placeholder: { + Circle() + } let introspectView = imageView .onSuccess { _, _, _ in expectation.fulfill() @@ -83,10 +87,6 @@ class WebImageTests: XCTestCase { } .onProgress { _, _ in - } - .placeholder(.init(platformImage: PlatformImage())) - .placeholder { - Circle() } // Image .resizable()