From b174fee26bef35c1bd7f330bb4e0436082c08867 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 6 May 2020 23:06:39 +0800 Subject: [PATCH 1/2] Add the same overload method for onSuccess API, which introduce the image data arg. Keep the source code compatibility --- README.md | 3 +- SDWebImageSwiftUI/Classes/AnimatedImage.swift | 37 ++++++++++++++++--- SDWebImageSwiftUI/Classes/ImageManager.swift | 27 ++++++++++++-- SDWebImageSwiftUI/Classes/WebImage.swift | 25 ++++++++++++- 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d19fe8d9..3377bde5 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,9 @@ github "SDWebImage/SDWebImageSwiftUI" var body: some View { WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic")) // Supports options and context, like `.delayPlaceholder` to show placeholder only when error - .onSuccess { image, cacheType in + .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 diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 81a8b885..b9e8b9b1 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -49,7 +49,7 @@ final class AnimatedLoadingModel : ObservableObject, IndicatorReportable { @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) final class AnimatedImageHandler: ObservableObject { // Completion Handler - @Published var successBlock: ((PlatformImage, SDImageCacheType) -> Void)? + @Published var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)? @Published var failureBlock: ((Error) -> Void)? @Published var progressBlock: ((Int, Int) -> Void)? // Coordinator Handler @@ -208,12 +208,15 @@ public struct AnimatedImage : PlatformViewRepresentable { return } self.imageLoading.isLoading = true - if imageModel.webOptions.contains(.delayPlaceholder) { + let options = imageModel.webOptions + if options.contains(.delayPlaceholder) { self.imageConfiguration.placeholderView?.isHidden = true } else { self.imageConfiguration.placeholderView?.isHidden = false } - view.wrapped.sd_setImage(with: imageModel.url, placeholderImage: imageConfiguration.placeholder, options: imageModel.webOptions, context: imageModel.webContext, progress: { (receivedSize, expectedSize, _) in + var context = imageModel.webContext ?? [:] + context[.animatedImageClass] = SDAnimatedImage.self + view.wrapped.sd_internalSetImage(with: imageModel.url, placeholderImage: imageConfiguration.placeholder, options: options, context: context, setImageBlock: nil, progress: { (receivedSize, expectedSize, _) in let progress: Double if (expectedSize > 0) { progress = Double(receivedSize) / Double(expectedSize) @@ -224,7 +227,7 @@ public struct AnimatedImage : PlatformViewRepresentable { self.imageLoading.progress = progress } self.imageHandler.progressBlock?(receivedSize, expectedSize) - }) { (image, error, cacheType, _) in + }) { (image, data, error, cacheType, finished, _) in // This is a hack because of Xcode 11.3 bug, the @Published does not trigger another `updateUIView` call // Here I have to use UIKit/AppKit API to triger the same effect (the window change implicitly cause re-render) if let hostingView = AnimatedImage.findHostingView(from: view) { @@ -241,7 +244,7 @@ public struct AnimatedImage : PlatformViewRepresentable { self.imageLoading.progress = 1 if let image = image { self.imageConfiguration.placeholderView?.isHidden = true - self.imageHandler.successBlock?(image, cacheType) + self.imageHandler.successBlock?(image, data, cacheType) } else { self.imageConfiguration.placeholderView?.isHidden = false self.imageHandler.failureBlock?(error ?? NSError()) @@ -702,11 +705,33 @@ extension AnimatedImage { return self } + /// Provide the action when image load successes. + /// - Parameters: + /// - action: The action to perform. The first arg is the loaded image. If `action` is `nil`, the call has no effect. + /// - Returns: A view that triggers `action` when this image load successes. + public func onSuccess(perform action: @escaping (PlatformImage) -> Void) -> AnimatedImage { + self.imageHandler.successBlock = { image, _, _ in + action(image) + } + return self + } + /// Provide the action when image load successes. /// - Parameters: /// - action: The action to perform. The first arg is the loaded image, the second arg is the cache type loaded from. If `action` is `nil`, the call has no effect. /// - Returns: A view that triggers `action` when this image load successes. - public func onSuccess(perform action: ((PlatformImage, SDImageCacheType) -> Void)? = nil) -> AnimatedImage { + public func onSuccess(perform action: @escaping (PlatformImage, SDImageCacheType) -> Void) -> AnimatedImage { + self.imageHandler.successBlock = { image, _, cacheType in + action(image, cacheType) + } + return self + } + + /// Provide the action when image load successes. + /// - Parameters: + /// - action: The action to perform. The first arg is the loaded image, the second arg is the loaded image data, the third arg is the cache type loaded from. If `action` is `nil`, the call has no effect. + /// - Returns: A view that triggers `action` when this image load successes. + public func onSuccess(perform action: ((PlatformImage, Data?, SDImageCacheType) -> Void)? = nil) -> AnimatedImage { self.imageHandler.successBlock = action return self } diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index e115d50d..f7416e37 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -17,6 +17,8 @@ public final class ImageManager : ObservableObject { @Published public var image: PlatformImage? /// loaded image data, may be nil if hit from memory cache. This will only published once even on incremental image loading @Published public var imageData: Data? + /// loaded image cache type, .none means from network + @Published public var cacheType: SDImageCacheType = .none /// loading error, you can grab the error code and reason listed in `SDWebImageErrorDomain`, to provide a user interface about the error reason @Published public var error: Error? /// whether network is loading or cache is querying, should only be used for indicator binding @@ -33,7 +35,7 @@ public final class ImageManager : ObservableObject { var url: URL? var options: SDWebImageOptions var context: [SDWebImageContextOption : Any]? - var successBlock: ((PlatformImage, SDImageCacheType) -> Void)? + var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)? var failureBlock: ((Error) -> Void)? var progressBlock: ((Int, Int) -> Void)? @@ -89,10 +91,11 @@ public final class ImageManager : ObservableObject { self.isIncremental = !finished if finished { self.imageData = data + self.cacheType = cacheType self.isLoading = false self.progress = 1 if let image = image { - self.successBlock?(image, cacheType) + self.successBlock?(image, data, cacheType) } else { self.failureBlock?(error ?? NSError()) } @@ -120,10 +123,28 @@ extension ImageManager { self.failureBlock = action } + /// Provide the action when image load successes. + /// - Parameters: + /// - action: The action to perform. The first arg is the loaded image. If `action` is `nil`, the call has no effect. + public func setOnSuccess(perform action: @escaping (PlatformImage) -> Void) { + self.successBlock = { image, _, _ in + action(image) + } + } + /// Provide the action when image load successes. /// - Parameters: /// - action: The action to perform. The first arg is the loaded image, the second arg is the cache type loaded from. If `action` is `nil`, the call has no effect. - public func setOnSuccess(perform action: ((PlatformImage, SDImageCacheType) -> Void)? = nil) { + public func setOnSuccess(perform action: @escaping (PlatformImage, SDImageCacheType) -> Void) { + self.successBlock = { image, _, cacheType in + action(image, cacheType) + } + } + + /// Provide the action when image load successes. + /// - Parameters: + /// - action: The action to perform. The first arg is the loaded image, the second arg is the loaded image data, the third arg is the cache type loaded from. If `action` is `nil`, the call has no effect. + public func setOnSuccess(perform action: ((PlatformImage, Data?, SDImageCacheType) -> Void)? = nil) { self.successBlock = action } diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index df53c291..96b89540 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -264,11 +264,34 @@ extension WebImage { return self } + /// Provide the action when image load successes. + /// - Parameters: + /// - action: The action to perform. The first arg is the loaded image. If `action` is `nil`, the call has no effect. + /// - Returns: A view that triggers `action` when this image load successes. + public func onSuccess(perform action: @escaping (PlatformImage) -> Void) -> WebImage { + let action = action + self.imageManager.successBlock = { image, _, _ in + action(image) + } + return self + } + /// Provide the action when image load successes. /// - Parameters: /// - action: The action to perform. The first arg is the loaded image, the second arg is the cache type loaded from. If `action` is `nil`, the call has no effect. /// - Returns: A view that triggers `action` when this image load successes. - public func onSuccess(perform action: ((PlatformImage, SDImageCacheType) -> Void)? = nil) -> WebImage { + public func onSuccess(perform action: @escaping (PlatformImage, SDImageCacheType) -> Void) -> WebImage { + self.imageManager.successBlock = { image, _, cacheType in + action(image, cacheType) + } + return self + } + + /// Provide the action when image load successes. + /// - Parameters: + /// - action: The action to perform. The first arg is the loaded image, the second arg is the loaded image data, the third arg is the cache type loaded from. If `action` is `nil`, the call has no effect. + /// - Returns: A view that triggers `action` when this image load successes. + public func onSuccess(perform action: ((PlatformImage, Data?, SDImageCacheType) -> Void)? = nil) -> WebImage { self.imageManager.successBlock = action return self } From f36440f508440ac20e9cbff901977bc88eb0ce68 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 6 May 2020 23:19:28 +0800 Subject: [PATCH 2/2] Update the test case about the new image data arg --- Tests/AnimatedImageTests.swift | 5 +++-- Tests/WebImageTests.swift | 31 +++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Tests/AnimatedImageTests.swift b/Tests/AnimatedImageTests.swift index 3b27f572..4d812db8 100644 --- a/Tests/AnimatedImageTests.swift +++ b/Tests/AnimatedImageTests.swift @@ -74,7 +74,8 @@ class AnimatedImageTests: XCTestCase { let expectation = self.expectation(description: "AnimatedImage url initializer") let imageUrl = URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif") let imageView = AnimatedImage(url: imageUrl) - .onSuccess { image, cacheType in + .onSuccess { image, data, cacheType in + XCTAssertNotNil(image) if let animatedImage = image as? SDAnimatedImage { XCTAssertEqual(animatedImage.animatedImageLoopCount, 0) XCTAssertEqual(animatedImage.animatedImageFrameCount, 389) @@ -88,7 +89,7 @@ class AnimatedImageTests: XCTestCase { ViewHosting.host(view: imageView) let animatedImageView = try imageView.inspect().actualView().platformView().wrapped XCTAssertEqual(animatedImageView.sd_imageURL, imageUrl) - self.waitForExpectations(timeout: 5, handler: nil) + self.waitForExpectations(timeout: 10, handler: nil) ViewHosting.expel() } diff --git a/Tests/WebImageTests.swift b/Tests/WebImageTests.swift index 6f17900d..524e59b4 100644 --- a/Tests/WebImageTests.swift +++ b/Tests/WebImageTests.swift @@ -21,7 +21,7 @@ class WebImageTests: XCTestCase { let expectation = self.expectation(description: "WebImage static url initializer") let imageUrl = URL(string: "https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png") let imageView = WebImage(url: imageUrl) - let introspectView = imageView.onSuccess { image, cacheType in + let introspectView = imageView.onSuccess { image, data, cacheType in #if os(iOS) || os(tvOS) let displayImage = try? imageView.inspect().group().image(0).uiImage() #else @@ -43,7 +43,7 @@ class WebImageTests: XCTestCase { let imageUrl = URL(string: "https://apng.onevcat.com/assets/elephant.png") let binding = Binding(wrappedValue: true) let imageView = WebImage(url: imageUrl, isAnimating: binding) - let introspectView = imageView.onSuccess { image, cacheType in + let introspectView = imageView.onSuccess { image, data, cacheType in if let animatedImage = image as? SDAnimatedImage { XCTAssertTrue(imageView.isAnimating) #if os(iOS) || os(tvOS) @@ -73,7 +73,7 @@ class WebImageTests: XCTestCase { 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 introspectView = imageView - .onSuccess { _, _ in + .onSuccess { _, _, _ in expectation.fulfill() } .onFailure { _ in @@ -111,7 +111,7 @@ class WebImageTests: XCTestCase { } func testWebImageOnSuccessWhenMemoryCacheHit() throws { - let expectation = self.expectation(description: "WebImage onSuccess when memory hit") + let expectation = self.expectation(description: "WebImage onSuccess when memory cache hit") let imageUrl = URL(string: "https://foo.bar/buzz.png") let cacheKey = SDWebImageManager.shared.cacheKey(for: imageUrl) #if os(macOS) @@ -121,7 +121,7 @@ class WebImageTests: XCTestCase { #endif SDImageCache.shared.storeImage(toMemory: testImage, forKey: cacheKey) let imageView = WebImage(url: imageUrl) - let introspectView = imageView.onSuccess { image, cacheType in + let introspectView = imageView.onSuccess { image, data, cacheType in XCTAssert(cacheType == .memory) XCTAssertNotNil(image) XCTAssertEqual(image, testImage) @@ -133,12 +133,31 @@ class WebImageTests: XCTestCase { ViewHosting.expel() } + func testWebImageOnSuccessWhenCacheMiss() throws { + let expectation = self.expectation(description: "WebImage onSuccess when cache miss") + let imageUrl = URL(string: "http://via.placeholder.com/100x100.png") + let cacheKey = SDWebImageManager.shared.cacheKey(for: imageUrl) + SDImageCache.shared.removeImageFromMemory(forKey: cacheKey) + SDImageCache.shared.removeImageFromDisk(forKey: cacheKey) + let imageView = WebImage(url: imageUrl) + let introspectView = imageView.onSuccess { image, data, cacheType in + XCTAssert(cacheType == .none) + XCTAssertNotNil(image) + XCTAssertNotNil(data) + expectation.fulfill() + } + _ = try introspectView.inspect() + ViewHosting.host(view: introspectView) + self.waitForExpectations(timeout: 5, handler: nil) + ViewHosting.expel() + } + func testWebImageEXIFImage() throws { let expectation = self.expectation(description: "WebImage EXIF image url") // EXIF 5, Left Mirrored let imageUrl = URL(string: "https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_5.jpg") let imageView = WebImage(url: imageUrl) - let introspectView = imageView.onSuccess { image, cacheType in + let introspectView = imageView.onSuccess { image, data, cacheType in let displayImage = try? imageView.inspect().group().image(0).cgImage() let orientation = try! imageView.inspect().group().image(0).orientation() XCTAssertNotNil(displayImage)