From d9fd4726eaf1a8816c765986de8b9d5d111e65f9 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 31 Mar 2020 17:24:47 +0800 Subject: [PATCH 1/5] Using a more trick but smart solution for cases when WebImage is used during transition state, like scaleEffect. In this time, we does not trigger a actualy image loading, only query the memory cache for quickly placeholder --- Example/SDWebImageSwiftUIDemo/DetailView.swift | 8 ++++---- SDWebImageSwiftUI/Classes/ImageManager.swift | 17 +++++++++++++++++ SDWebImageSwiftUI/Classes/WebImage.swift | 16 +++++++++------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Example/SDWebImageSwiftUIDemo/DetailView.swift b/Example/SDWebImageSwiftUIDemo/DetailView.swift index eacbbd29..f3cf15ce 100644 --- a/Example/SDWebImageSwiftUIDemo/DetailView.swift +++ b/Example/SDWebImageSwiftUIDemo/DetailView.swift @@ -38,7 +38,7 @@ struct DetailView: View { let url: String let animated: Bool @State var isAnimating: Bool = true - @State var lastScaleValue: CGFloat = 1.0 + @State var lastScale: CGFloat = 1.0 @State var scale: CGFloat = 1.0 @Environment(\.presentationMode) var presentationMode @EnvironmentObject var settings: UserSettings @@ -75,12 +75,12 @@ struct DetailView: View { return contentView() .scaleEffect(self.scale) .gesture(MagnificationGesture(minimumScaleDelta: 0.1).onChanged { value in - let delta = value / self.lastScaleValue - self.lastScaleValue = value + let delta = value / self.lastScale + self.lastScale = value let newScale = self.scale * delta self.scale = min(max(newScale, 0.5), 2) }.onEnded { value in - self.lastScaleValue = 1.0 + self.lastScale = 1.0 }) #endif #if os(tvOS) diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index 77267698..a148a2e8 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -106,6 +106,23 @@ public final class ImageManager : ObservableObject { } } + /// Prefetch the initial state of image + internal func prefetch() { + let key = manager.cacheKey(for: url) + if let imageCache = manager.imageCache as? SDImageCache { + self.image = imageCache.imageFromMemoryCache(forKey: key) + } else { + // generic API + manager.imageCache.containsImage(forKey: key, cacheType: .memory) { [unowned self] (cacheType) in + if cacheType == .memory { + self.manager.imageCache.queryImage(forKey: key, options: self.options, context: self.context) { [unowned self] (image, data, cacheType) in + self.image = image + } + } + } + } + } + } // Completion Handler diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 424552de..bbb04ae4 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -57,16 +57,13 @@ public struct WebImage : View { } } self.imageManager = ImageManager(url: url, options: options, context: context) + // this prefetch the memory cache of image, to immediately render it on screen + // this solve the cause when `onAppear` not been called, for example, some transaction indetermite state :) + self.imageManager.prefetch() } public var body: some View { - // load remote image when first called `body`, SwiftUI sometimes will create a new View struct without calling `onAppear` (like enter EditMode) :) - // this can ensure we load the image, and display image synchronously when memory cache hit to avoid flashing - // called once per struct, SDWebImage take care of the duplicated query - if imageManager.isFirstLoad { - imageManager.load() - } - return Group { + Group { if imageManager.image != nil { if isAnimating && !self.imageManager.isIncremental { if currentFrame != nil { @@ -109,6 +106,11 @@ public struct WebImage : View { setupPlaceholder() .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .onAppear { + // load remote image when first appear + if self.imageManager.isFirstLoad { + self.imageManager.load() + return + } guard self.retryOnAppear else { return } // When using prorgessive loading, the new partial image will cause onAppear. Filter this case if self.imageManager.image == nil && !self.imageManager.isIncremental { From 94aadc1927d4563bd795613edffbb2e4dd372fae Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 31 Mar 2020 20:02:21 +0800 Subject: [PATCH 2/5] Fix the issue when using thumbnailPixelSize and optionsProcessor --- SDWebImageSwiftUI/Classes/ImageManager.swift | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index a148a2e8..c21458f1 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -106,18 +106,20 @@ public final class ImageManager : ObservableObject { } } - /// Prefetch the initial state of image - internal func prefetch() { - let key = manager.cacheKey(for: url) - if let imageCache = manager.imageCache as? SDImageCache { - self.image = imageCache.imageFromMemoryCache(forKey: key) - } else { - // generic API - manager.imageCache.containsImage(forKey: key, cacheType: .memory) { [unowned self] (cacheType) in - if cacheType == .memory { - self.manager.imageCache.queryImage(forKey: key, options: self.options, context: self.context) { [unowned self] (image, data, cacheType) in - self.image = image - } + /// Prefetch the initial state of image, currently query the memory cache only + func prefetch() { + // Use the options processor if provided + var context = self.context + if let result = manager.optionsProcessor?.processedResult(for: url, options: options, context: context) { + context = result.context + } + // TODO: before SDWebImage 5.7.0, this is the SPI. Remove later + let key = manager.perform(Selector(("cacheKeyForURL:context:")), with: url, with: context)?.takeUnretainedValue() as? String + // This callback is synchronzied + manager.imageCache.containsImage(forKey: key, cacheType: .memory) { [unowned self] (cacheType) in + if cacheType == .memory { + self.manager.imageCache.queryImage(forKey: key, options: self.options, context: self.context) { [unowned self] (image, data, cacheType) in + self.image = image } } } From 78d9bfbb6003f5a6d8e49b37e522b4c23bea8f24 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 31 Mar 2020 20:21:08 +0800 Subject: [PATCH 3/5] Fix the context arg pass issue, should use the local variable --- SDWebImageSwiftUI/Classes/ImageManager.swift | 3 ++- SDWebImageSwiftUI/Classes/WebImage.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index c21458f1..572b683f 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -109,6 +109,7 @@ public final class ImageManager : ObservableObject { /// Prefetch the initial state of image, currently query the memory cache only func prefetch() { // Use the options processor if provided + let options = self.options var context = self.context if let result = manager.optionsProcessor?.processedResult(for: url, options: options, context: context) { context = result.context @@ -118,7 +119,7 @@ public final class ImageManager : ObservableObject { // This callback is synchronzied manager.imageCache.containsImage(forKey: key, cacheType: .memory) { [unowned self] (cacheType) in if cacheType == .memory { - self.manager.imageCache.queryImage(forKey: key, options: self.options, context: self.context) { [unowned self] (image, data, cacheType) in + self.manager.imageCache.queryImage(forKey: key, options: options, context: context) { [unowned self] (image, data, cacheType) in self.image = image } } diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index bbb04ae4..c22c6e25 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -58,7 +58,7 @@ public struct WebImage : View { } self.imageManager = ImageManager(url: url, options: options, context: context) // this prefetch the memory cache of image, to immediately render it on screen - // this solve the cause when `onAppear` not been called, for example, some transaction indetermite state :) + // this solve the case when `onAppear` not been called, for example, some transaction indeterminate state, SwiftUI :) self.imageManager.prefetch() } From aafa7a2222c94012ad81e8806f1baa03e98a1490 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 31 Mar 2020 21:08:01 +0800 Subject: [PATCH 4/5] Fix the case that user should get the onSuccess callback even when memory cache hit --- SDWebImageSwiftUI/Classes/ImageManager.swift | 5 +++++ SDWebImageSwiftUI/Classes/WebImage.swift | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index 572b683f..5d7a767a 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -27,6 +27,7 @@ public final class ImageManager : ObservableObject { var manager: SDWebImageManager weak var currentOperation: SDWebImageOperation? = nil var isFirstLoad: Bool = true // false after first call `load()` + var isFirstPrefetch: Bool = true // false after first call `prefetch()` var url: URL? var options: SDWebImageOptions @@ -108,6 +109,7 @@ public final class ImageManager : ObservableObject { /// Prefetch the initial state of image, currently query the memory cache only func prefetch() { + isFirstPrefetch = false // Use the options processor if provided let options = self.options var context = self.context @@ -121,6 +123,9 @@ public final class ImageManager : ObservableObject { if cacheType == .memory { self.manager.imageCache.queryImage(forKey: key, options: options, context: context) { [unowned self] (image, data, cacheType) in self.image = image + if let image = image { + self.successBlock?(image, cacheType) + } } } } diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index c22c6e25..ff10e021 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -57,13 +57,15 @@ public struct WebImage : View { } } self.imageManager = ImageManager(url: url, options: options, context: context) - // this prefetch the memory cache of image, to immediately render it on screen - // this solve the case when `onAppear` not been called, for example, some transaction indeterminate state, SwiftUI :) - self.imageManager.prefetch() } public var body: some View { - Group { + // this prefetch the memory cache of image, to immediately render it on screen + // this solve the case when `onAppear` not been called, for example, some transaction indeterminate state, SwiftUI :) + if imageManager.isFirstPrefetch { + self.imageManager.prefetch() + } + return Group { if imageManager.image != nil { if isAnimating && !self.imageManager.isIncremental { if currentFrame != nil { From ffeea1a3343f9a3304bdf3e7e01f026f513f8723 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 1 Apr 2020 11:22:22 +0800 Subject: [PATCH 5/5] Fix the edge cases when transformer and thumbnail get applied at the same time, we need to write correct code to query memory cache only --- SDWebImageSwiftUI/Classes/ImageManager.swift | 31 +++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index 5d7a767a..5c913648 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -116,15 +116,30 @@ public final class ImageManager : ObservableObject { if let result = manager.optionsProcessor?.processedResult(for: url, options: options, context: context) { context = result.context } + // TODO: Remove transformer for cache calculation before SDWebImage 5.7.0, this is bug. Remove later + let transformer = (context?[.imageTransformer] as? SDImageTransformer) ?? manager.transformer + context?[.imageTransformer] = nil // TODO: before SDWebImage 5.7.0, this is the SPI. Remove later - let key = manager.perform(Selector(("cacheKeyForURL:context:")), with: url, with: context)?.takeUnretainedValue() as? String - // This callback is synchronzied - manager.imageCache.containsImage(forKey: key, cacheType: .memory) { [unowned self] (cacheType) in - if cacheType == .memory { - self.manager.imageCache.queryImage(forKey: key, options: options, context: context) { [unowned self] (image, data, cacheType) in - self.image = image - if let image = image { - self.successBlock?(image, cacheType) + var key = manager.perform(Selector(("cacheKeyForURL:context:")), with: url, with: context)?.takeUnretainedValue() as? String + if let transformer = transformer { + key = SDTransformedKeyForKey(key, transformer.transformerKey) + } + // Shortcut for built-in cache + if let imageCache = manager.imageCache as? SDImageCache { + let image = imageCache.imageFromMemoryCache(forKey: key) + self.image = image + if let image = image { + self.successBlock?(image, .memory) + } + } else { + // This callback is synchronzied + manager.imageCache.containsImage(forKey: key, cacheType: .memory) { [unowned self] (cacheType) in + if cacheType == .memory { + self.manager.imageCache.queryImage(forKey: key, options: options, context: context) { [unowned self] (image, data, cacheType) in + self.image = image + if let image = image { + self.successBlock?(image, cacheType) + } } } }