Firebase 存储异步图像下载随机顺序
Posted
技术标签:
【中文标题】Firebase 存储异步图像下载随机顺序【英文标题】:Firebase Storage async image download random order 【发布时间】:2020-01-27 21:42:01 【问题描述】:我正在尝试从 firebase 存储下载图像以显示在我的 collectionview 单元格中,但图像继续以随机顺序出现在单元格中。每个单元格都有一个从 firebase 存储(item1、item2 等)中检索到的标签,该标签每次都很好地显示在正确的单元格中。存储在 firebase 存储中的图像每个都将其存储 url 作为其在 firebase 数据库中各自项目名称的子项。
我成功地检索到每个图像 url,并下载所有图像并正确显示在单元格中,只是每次打开应用程序时它们都会以随机顺序出现,因此图像与项目名称标签。
我意识到我需要异步下载图像,因此每个图像在继续下一个之前完成加载到正确的单元格中,但我在这样做时遇到了麻烦。到目前为止,这是我的代码:
func downloadImg(completion: @escaping (UIImage?) -> ())
let ref = Database.database().reference().child("somePath")
ref.observeSingleEvent(of: .value) (snapshot) in
for item in snapshot.children
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "img/storageUrl")
if let url = imageSnap.value as? String
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) data, error in
if let error = error
print(error)
else
let image = UIImage(data: data!)
DispatchQueue.main.async
completion(image)
然后我在viewdidload中调用我的函数:
downloadImg (completion) in
self.itemPicArray.append(completion!)
self.collectionView?.reloadData()
最后我将我的单元格图像视图设置为itemPicArray[indexPath.row]
就像我说的那样,这非常有效,只是图像不断随机显示。非常感谢帮助!
【问题讨论】:
看看developer.apple.com/documentation/dispatch/dispatchgroup。在下载下一个图像之前确保图像已经下载;而您当前的想法只是同时下载所有内容,并会给出不同的结果。 我试过这个,创建一个 DispatchGroup 实例并使用 .enter 和 .leave 但这似乎什么也没做。我应该改用 .wait 吗?还是别的什么? 我假设您还在 DispatchGroup 上使用了 .notify?在循环中进行异步调用时,我在使用 DispatchGroup 时遇到了困难。你在哪里 .leave() 你的 for-in-loop? 我没有,下次试试。我在函数的开头使用了 .enter,在 someRef.getData 中的 else 语句之后使用了 .leave。我对异步编程非常缺乏经验,所以我不太确定应该在哪里放置什么以及包含什么。 原谅这种糟糕的编辑风格;我在一个牢房里。我没有试过你的代码。但我想到的是:group.enter() 放在循环的顶部; group.leave() 放在 someRef.getData() 的右括号之前; group.notify() 放在循环的右括号之后。我猜你需要将你的图片作为 [UIImage](而不是 UIImage)发回,并相应地改变你的调用,比如 self.itemPicArray = completion。 【参考方案1】:您的问题是每次图像进入时,您都会重新加载整个集合视图。根据图像的大小和网络的状态,图像几乎每次都会以不同的顺序出现。
考虑先下载所有图像,然后重新加载一次集合视图。如果有很多图像,请考虑对结果进行分页。您可以枚举循环并按此原始顺序对数据源数组进行排序。我添加了一个自定义数据对象来帮助解决这个问题。
class CustomObject
var image: UIImage?
let n: Int
init(image: UIImage?, n: Int)
self.image = image
self.n = n
let dispatch = DispatchGroup()
for (n, item) in snapshot.children.enumerated()
let object = CustomObject(image: nil, n: n) // init custom object with n (image is still nil)
dispatch.enter() // enter dispatch
someRef.getData(maxSize: 10 * 10024 * 10024) data, error in // download image
if let error = error
print(error)
else
let image = UIImage(data: data!)
object.image = image // inject custom object with image
itemPicArray.append(object) // append to array
dispatch.leave() // leave dispatch
dispatch.notify(queue: .global()) // dispatch completion
itemPicArray.sort $0.n < $1.n // sort by n (original download order)
collectionView.reloadData() // reload collection view
【讨论】:
感谢您的评论!我还有一些问题 - 我应该在哪里调用 collectionview.reloaddata 所以它只被调用一次?我也试过你的代码,但在'itemPicArray.append(object)'行它给出了错误“无法将'CollectionViewCell.CustomObject'类型的值转换为预期的参数类型'UIImage'”。我应该使用字典来存储图像吗? 您需要将您的数组更新为CustomObject
类型(或自定义对象的最终类型)。并且集合视图在调度通知函数中重新加载(我的代码中的最后一行)。该函数在所有调度进入和离开(下载调用和实际图像下载)完成后调用一次。
您仍然需要您的字符串数组,对吧?保留您的 itemPicArray - 我假设它是 [String]() 类型 - 并创建 CustomObject 类型的新数组。
太好了,我已经实现了您的代码并且没有错误,但是如何在我的 collectionview 单元格中显示存储在 CustomObject 数组中的图像?另外我知道您将下载的图像作为 CustomObjects 图像属性,但是 n 属性呢?我们不需要也给每张图片一个 int n 吗?
我假设 cell.cellImg.image = itemPicArray[indexPath.row].image 将是显示图像的正确方式?我这样做了,没有错误,但单元格中也没有图像。我的数组如下所示: var itemPicArray = [CustomObject]()【参考方案2】:
使用模型可能是个好主意。
struct Image
var imageName: String
var image: UIImage
这样,无论顺序如何,商品名称(图片名称)和图片都会配对。
也许现在更好的解决方案是配置方法 downloadImg 使其将 imageName 作为参数。然后就可以调用正确的节点获取对应的storageURL了。
func downloadImg(imageName: String, completion: @escaping (Image?) -> ())
// Use the parameter to create your database reference
let ref = Database.database().reference().child(imageName)
ref.observeSingleEvent(of: .value) (snapshot) in
for item in snapshot.children
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "img/storageUrl")
if let url = imageSnap.value as? String
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) data, error in
if let error = error
print(error)
return
if let image = UIImage(data: data)
// Create a variable of type Image (your custom model)
let imageWithName = Image(imageName: imageName, image: image)
completion(imageWithName)
调用和处理可以这样完成:
// Create a variable to hold your item name/image-pairs
var imagesWithNames = [Image]()
let dispatchGroup = DispatchGroup()
// Iterate over your array of item names
for item in itemArray
dispatchGroup.enter()
downloadImg(item) (imageWithName) in
self.imagesWithNames.append(imageWithName)
dispatchGroup.leave()
dispatchGroup.notify(queue: .main)
self.collectionView?.reloadData()
要填充collectionView,你可以去:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! YourCustomCell
// Get the pair at the given index
let imageWithName = self.imagesWithNames[indexPath.row]
DispatchQueue.main.async
// Set image and item label (example below)
self.yourImageView.image = imageWithName.image
self.yourItemLabel.text = imageWithName.imageName
return cell
【讨论】:
感谢您的回答!我尝试完全按照您在我的代码中所说的那样做,但不幸的是我遇到了与以前完全相同的问题。我已经尝试了许多不同的方法来做到这一点,并查看了论坛上的许多其他帖子,但我似乎仍然无法让它发挥作用。太令人沮丧了! :( 啊,太糟糕了!!你如何填充你的收藏视图?更具体地说,您在哪里以及如何将名称与图像“配对”? 对于名称,我从 fb db 检索所有项目名称并将其放入数组中。然后我使用 itemArray[indexPath.row] 在每个单元格中正确显示名称。在数据库中,每个“itemName”节点都有一个子“img”,其中有一个子节点,其键为“storageUrl”,而 url 是键。所以我试图对图像和项目名称做同样的事情——创建一个数组并使用 indexPath.row 来填充单元格。图像和项目名称之间的唯一“配对”是图像 url 在数据库中正确的项目名称内 - 也许这就是问题所在?也许我需要将两者以不同的方式配对? 碰巧用一个新答案抹去了我之前的答案。 @codeski 如果你还在苦苦挣扎我编辑了我的答案【参考方案3】:如果将来有人对此有疑问,请使用 -bsod answer 并创建一个自定义对象。还要创建一个变量 var counter: Int = 0。这是我的代码看起来像 Swift 5.X 的样子,它运行良好。
class CustomObject
var image: UIImage?
let n: Int
init(image: UIImage?, n: Int)
self.image = image
self.n = n
func reloadStuff()
dispatch.notify(queue: .main)
self.imageArrayCells.sort $0.n < $1.n
self.contentViewProfile.collectionView.refreshControl?.endRefreshing()
self.contentViewProfile.collectionView.reloadData()
for i in 0 ..< self.imageArrayInfo.count
let object = CustomObject(image: nil, n: i)
let urlString = self.imageArrayInfo[i]
print(object.n)
let url = URL(string: urlString)
self.dispatch.enter()
let task = URLSession.shared.dataTask(with: url!) (data, response, error) in
self.dispatch.enter()
guard let data = data, error == nil else
return
guard let image = UIImage(data: data) else
return
object.image = image
self.imageArrayCells.append(object)
self.dispatch.leave()
self.counter += 1
if self.counter == self.imageArrayInfo.count
for i in 0 ..< self.imageArrayInfo.count
self.dispatch.leave()
self.counter = 0
task.resume()
self.reloadStuff()
这就是我在 collectionView(cellForItemAt:_) 中调用的内容
cell.imageInProfileCollection.image = imageArrayCells[indexPath.row].image
【讨论】:
以上是关于Firebase 存储异步图像下载随机顺序的主要内容,如果未能解决你的问题,请参考以下文章
无法在 Firebase Firestore 中存储下载 URL