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

以与文件相同的顺序加载图像

随机化 Firebase 存储中的图像

将多个图像的异步图像上传到firebase存储

Flutter:Fire Base 存储图像上传的后期初始化错误

Firebase存储安全规则为特定用户提供访问权限