我是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
我不确定为什么图像会一遍又一遍地加载(至今)。我肯定想念一些简单的东西,但是对于组合的方式,我绝对不明智。任何见解或解决方案将不胜感激。
答案 0 :(得分: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()
}
}
答案 1 :(得分:0)
// 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)
从您的布局来看,可能是最好的方法
或仅将.frame(width:60, height:60)
从ImageView中移出并将其应用到您的列表中
ImageView(withURL: item.artworkUrl60)
.frame(width:60, height:60)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
我还将尝试固定所有List组件的大小...,直到找到导致闪烁的组件。
答案 2 :(得分:0)
我正在使用的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
答案 3 :(得分:0)
我在处理远程图像时遇到了类似的问题。
我使用像您这样的 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 方法,因此缓存类还有改进的空间,但这是一个很好的起点。