SwiftUI 列表 - 远程图像加载 = 图像闪烁(重新加载)
Posted
技术标签:
【中文标题】SwiftUI 列表 - 远程图像加载 = 图像闪烁(重新加载)【英文标题】:SwiftUI List - Remote Image loading = images flickering (reloading) 【发布时间】:2020-02-21 20:46:35 【问题描述】:我是 SwiftUI 和 Combine 的新手。我有一个简单的列表,我正在加载 iTunes 数据并构建列表。加载图像有效 - 但它们在闪烁,因为似乎我在主线程上的调度一直在触发。我不确定为什么。下面是图像加载的代码,然后是它的实现位置。
struct ImageView: View
@ObservedObject var imageLoader: ImageLoaderNew
@State var image: UIImage = UIImage()
init(withURL url: String)
imageLoader = ImageLoaderNew(urlString: url)
func imageFromData(_ data: Data) -> UIImage
UIImage(data: data) ?? UIImage()
var body: some View
VStack
Image(uiImage: imageLoader.dataIsValid ?
imageFromData(imageLoader.data!) : UIImage())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:60, height:60)
.background(Color.gray)
class ImageLoaderNew: ObservableObject
@Published var dataIsValid = false
var data: Data?
// The dispatch fires over and over again. No idea why yet
// this causes flickering of the images in the List.
// I am only loading a total of 3 items.
init(urlString: String)
guard let url = URL(string: urlString) else return
let task = URLSession.shared.dataTask(with: url) data, response, error in
guard let data = data else return
DispatchQueue.main.async
self.dataIsValid = true
self.data = data
print(response?.url as Any) // prints over and over again.
task.resume()
这里是加载JSON等后实现的。
List(results, id: \.trackId) item in
HStack
// All of these image end up flickering
// every few seconds or so.
ImageView(withURL: item.artworkUrl60)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
VStack(alignment: .leading)
Text(item.trackName)
.foregroundColor(.gray)
.font(.headline)
.truncationMode(.middle)
.lineLimit(2)
Text(item.collectionName)
.foregroundColor(.gray)
.font(.caption)
.truncationMode(.middle).lineLimit(1)
.frame(height: 200.0)
.onAppear(perform: loadData) // Only happens once
我不确定为什么图像会一遍又一遍地加载(至今)。我一定遗漏了一些简单的东西,但我绝对不知道组合的方式。任何见解或解决方案将不胜感激。
【问题讨论】:
【参考方案1】:我将更改分配顺序如下(因为否则dataIsValid
强制刷新,但尚未设置data
)
DispatchQueue.main.async
self.data = data // 1) set data
self.dataIsValid = true. // 2) notify that data is ready
其他一切似乎都不重要。但是考虑以下优化的可能性,因为UIImage
从数据构造可能也足够长(对于 UI 线程)
func imageFromData(_ data: Data) -> UIImage
UIImage(data: data) ?? UIImage()
所以我建议不仅将数据,而且将整个图像构造移动到后台线程中,并仅在最终图像准备好时刷新。
更新:我已使您的代码快照在本地工作,当然,由于并非所有部分最初都可用,因此进行了一些替换和简化,并建议进行上述修复。以下是一些实验结果:
如您所见,没有观察到闪烁,也没有重复登录控制台。由于我没有对您的代码逻辑进行任何重大更改,我认为导致报告闪烁的问题不在提供的代码中 - 可能其他一些部分导致重新创建 ImaveView
从而产生这种效果。
这是我的代码(完全,用 Xcode 11.2/3 和 ios 13.2/3 测试):
struct ImageView: View
@ObservedObject var imageLoader: ImageLoaderNew
@State var image: UIImage = UIImage()
init(withURL url: String)
imageLoader = ImageLoaderNew(urlString: url)
func imageFromData(_ data: Data) -> UIImage
UIImage(data: data) ?? UIImage()
var body: some View
VStack
Image(uiImage: imageLoader.dataIsValid ?
imageFromData(imageLoader.data!) : UIImage())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:60, height:60)
.background(Color.gray)
class ImageLoaderNew: ObservableObject
@Published var dataIsValid = false
var data: Data?
// The dispatch fires over and over again. No idea why yet
// this causes flickering of the images in the List.
// I am only loading a total of 3 items.
init(urlString: String)
guard let url = URL(string: urlString) else return
let task = URLSession.shared.dataTask(with: url) data, response, error in
guard let data = data else return
DispatchQueue.main.async
self.data = data
self.dataIsValid = true
print(response?.url as Any) // prints over and over again.
task.resume()
struct TestImageFlickering: View
@State private var results: [String] = []
var body: some View
NavigationView
List(results, id: \.self) item in
HStack
// All of these image end up flickering
// every few seconds or so.
ImageView(withURL: item)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
VStack(alignment: .leading)
Text("trackName")
.foregroundColor(.gray)
.font(.headline)
.truncationMode(.middle)
.lineLimit(2)
Text("collectionName")
.foregroundColor(.gray)
.font(.caption)
.truncationMode(.middle).lineLimit(1)
.frame(height: 200.0)
.onAppear(perform: loadData)
// Only happens once
func loadData()
var urls = [String]()
for i in 1...10
urls.append("https://placehold.it/120.png&text=image\(i)")
self.results = urls
struct TestImageFlickering_Previews: PreviewProvider
static var previews: some View
TestImageFlickering()
【讨论】:
我切换了顺序 - 它仍然被一遍又一遍地调用(print(response?.url as Any)。其他事情正在发生。 @Asperi 更改顺序无济于事,但“我建议不仅将数据,而且将整个图像构造移动到后台线程并仅在最终图像准备好时刷新”可能会有所帮助,如果最终图像将被创建为固定大小【参考方案2】:// The dispatch fires over and over again. No idea why yet
// this causes flickering of the images in the List.
// I am only loading a total of 3 items.
init(urlString: String) ....
表示init一遍遍地调用
只能从这部分调用
// All of these image end up flickering
// every few seconds or so.
ImageView(withURL: item.artworkUrl60)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
在什么情况下 SwiftUI 会这样做?除了改变布局,我什么也没看到。看来,调整 ImageView 的大小是发现这种闪烁的地方。
我会尝试玩这部分
.resizable()
.aspectRatio(contentMode: .fit)
或申请
ImageView(withURL: item.artworkUrl60)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
.fixedSize()
Asperi 的建议(从后台下载的数据创建固定大小的图像)并删除
.resizable()
.aspectRatio(contentMode: .fit)
从你的布局来看,可能是最好的方法
或者只是从 ImageView 中移动 .frame(width:60, height:60)
并将其应用到您的列表中
ImageView(withURL: item.artworkUrl60)
.frame(width:60, height:60)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
我还将尝试修复所有 List 组件的大小...,直到您找到哪个使闪烁。
【讨论】:
【参考方案3】:我在使用的 SwiftUI 远程图像加载器中遇到了类似的问题。尝试使 ImageView
相等,以避免在 uiImage 设置为初始化的 nil 值以外的值或返回 nil 后重绘。
struct ImageView: View, Equatable
static func == (lhs: ImageView, rhs: ImageView) -> Bool
let lhsImage = lhs.image
let rhsImage = rhs.image
if (lhsImage == nil && rhsImage != nil) || (lhsImage != nil && rhsImage == nil)
return false
else
return true
@ObservedObject var imageLoader: ImageLoaderNew
@State var image: UIImage?
// ... rest the same
【讨论】:
【参考方案4】:我在处理远程图像时遇到了类似的问题。
我使用像你这样的 ObservableObject 作为图像加载器,发现这就是问题所在。 SwiftUI 会多次创建 ImageView 结构,如果您实例化一个新的 ImageLoader 并在每次最终收到数百个 URLSession 请求时加载数据。即使网络请求被缓存,你也必须将 Data 转换为 UIImage 数百次。
我通过在单独的类中实现缓存机制来解决闪烁问题。 我仍然使用 ImageLoader,所以我可以为我的 SwiftUI 视图创建一个 ObservableObject,但我没有在那里创建 URLSession 数据任务,我从不同的对象获取 UIImage 并且 UIImage 本身被缓存,而不是来自网络请求的数据.
class ImageLoader: ObservableObject
@Published var image = UIImage()
func load(url:URL)
loadImage(fromURL: url)
func load(urlString:String)
guard let url = URL(string: urlString) else return
loadImage(fromURL: url)
// MARK: - Private
private var imageCache = ImageCache.shared
private func loadImage(fromURL url:URL)
imageCache.imageForURL(url) image in
if let image = image
DispatchQueue.main.async
self.image = image
而实际的图片加载是在另一个类中完成的
typealias ImageCacheCompletion = (UIImage?) -> Void
class ImageCache
static let shared = ImageCache()
func imageForURL(_ url:URL, completion: @escaping ImageCacheCompletion)
if let cachedImage = cache.first(where: record in
record.urlString == url.absoluteString
)
completion(cachedImage.image)
else
loadImage(url: url, completion: completion)
// MARK: - Private
private var cache:[ImageCacheRecord] = []
private let cacheSize = 10
private func imageFromData(_ data:Data) -> UIImage?
UIImage(data: data)
private func loadImage(url:URL, completion: @escaping ImageCacheCompletion)
RESTClient.loadData(atURL: url) result in
switch result
case .success(let data):
if let image = self.imageFromData(data)
completion(image)
self.setImage(image, forUrl: url.absoluteString)
else
completion(nil)
case .failure(_ ):
completion(nil)
private func setImage(_ image:UIImage, forUrl url:String)
if cache.count >= cacheSize
cache.remove(at: 0)
cache.append(ImageCacheRecord(image: image, urlString: url))
// MARK: - ImageCacheRecord
struct ImageCacheRecord
var image:UIImage
var urlString:String
这个实现的一个例子是GitHub
您可能需要调整一些参数,例如缓存大小以满足您的需求,正如您所见,我使用 FIFO 方法,因此缓存类有改进的空间,但这是一个很好的起点。
【讨论】:
以上是关于SwiftUI 列表 - 远程图像加载 = 图像闪烁(重新加载)的主要内容,如果未能解决你的问题,请参考以下文章
远程图像使用 List 正确加载,但在使用带有嵌入式 VStack 的 ScrollView 和 SwiftUI 时不加载
为啥从 Realm DB 加载图像时 SwiftUI List 会崩溃? RAM 达到极限