0

i am trying to fetch images data using URLSession dataTask the urls are fetched from a firebase firestore document that contains each download path using for loop in snapShotDocuments in ascending order, after that the urls are passed into the URLSession dataTask that retrieves the data then appending the result in an array tableCells[] to update a tableview, the problem is the order of the cells in the updated tableview is not the same order of the objects in tableCells array, i am expecting it has something to do with concurrency that i am not aware of here is my code


public func fetchCells() {
        
        guard (UserDefaults.standard.value(forKeyPath: "email") as? String) != nil else {
            return
        }
        
        spinner.textLabel.text = "Loading"
        spinner.position = .center
        spinner.show(in: tableView)
        
        db.collection("ads").order(by: "timeStamp").addSnapshotListener { snapshot, error in
            
            self.tableCells = []
            
            guard error == nil , let snapShotDocuments = snapshot?.documents else {
                return
            }
            guard !snapShotDocuments.isEmpty else {
                print("snapshot is empty ")
                DispatchQueue.main.async {
                    self.tableView.isHidden = true
                    self.spinner.dismiss()
                }
                return
            }
            
            for i in snapShotDocuments {
                
                let documentData = i.data()
                
                guard let imageURL = documentData["imageurl"] as? String , let imageStringURL = URL(string: imageURL) else {
                    print("no url ")
                    return
                }
                
                guard let descriptionLabel = documentData["adDescription"] as? String , let titleLabel = documentData["adTitle"] as? String , let timeStamp = documentData["timeStamp"] as? Double else {
                    print("error")
                    return
                }
                 
                URLSession.shared.dataTask(with: imageStringURL) { data , _ , error  in
                    guard error == nil , let data = data else {
                        return
                    }
                    
                    let image = UIImage(data: data)
                    let newCell = adoptionCell(cellImage: image, descriptionLabel: descriptionLabel, titleLabel: titleLabel, timePosted: timeStamp, imageUrl: nil)
                    self.tableCells.append(newCell)
                    
                    DispatchQueue.main.async {
                        self.tableView.reloadData()
                        self.spinner.dismiss()
                    }
                }.resume()
            }
        }
    }

2 Answers 2

0

yes correct some image might be loaded faster another is loaded slower. therefore position in final array is changed.

I would rather access tableCells in main thread. here I reload cells in batch. index is used for setting position of the cell in final array.

var tableCells = Array<TableCell?>(repeating: nil, count: snapShotDocuments.count) //preserve space for cells...
                var count: Int32 = 0 // actual number of real load tasks
                
                for tuple in snapShotDocuments.enumerated() {
                    
                    let i = tuple.element
                    let index = tuple.offset //offset of cell in final array.
                    
                    let documentData = i.data()
                    
                    guard let imageURL = documentData["imageurl"] as? String , let imageStringURL = URL(string: imageURL) else {
                        print("no url ")
                        return
                    }
                    
                    guard let descriptionLabel = documentData["adDescription"] as? String , let titleLabel = documentData["adTitle"] as? String , let timeStamp = documentData["timeStamp"] as? Double else {
                        print("error")
                        return
                    }
                    count += 1 //increment count as there is new task..
                    URLSession.shared.dataTask(with: imageStringURL) { data , _ , error  in
                        if error == nil, let data = data {
                            let image = UIImage(data: data)
                            let newCell = adoptionCell(cellImage: image, descriptionLabel: descriptionLabel, titleLabel: titleLabel, timePosted: timeStamp, imageUrl: nil)
                            //self.tableCells.append(newCell)
                            tableCells[index] = newCell //because array has predefined capacity, thread safe...
                        }
                        
                        guard OSAtomicDecrement32(&count) == 0 else { return }
                        //last task, then batch reload..

                        DispatchQueue.main.async { [weak self] in
                            guard let self = self else { return }
                            self.tableCells = tableCells.compactMap { $0 }
                            self.tableView.reloadData()
                            self.spinner.dismiss()
                        }
                    }.resume()
                    
                }
Sign up to request clarification or add additional context in comments.

2 Comments

Avoid using the index property for this, that's too can change under your feet - like what if the table view lets the user delete something. Now the indices are broken if user deletes a row while we are fetching its image. A url, or some fixed identifier is best. Something like an ordered dictionary keyed by model id.
@Shadowrun in some way you are right, but originally there were not anything about ui interaction and so on..
0

What you have:

for i in snapShotDocuments {
   dataTask { 
     mutate tableCells (append) on background thread <- don't do that, A) not thread safe, and B) append won't happen in order they were dispatched, but the order they came back
     dispatch back to main {
        reload data  <- don't do that, reload the individual rows if needed, or reload everything at the end
     } 
}

You're enqueuing a number of asynchronous operations that can take varying amount of time to complete. Enqueue them in order 1, 2, 3, 4 and they could come back in order 3, 1, 4, 2, for example.

What you want:

Your model, arranged data instances, let's say an array, of structs, not UITableViewCell's.

for i in snapShotDocuments {
   dataTask {
     process on background thread, but then
     dispatch back to main {
        look up in the model, the object for which we have the new data
        mutate the model array
        then reload row at index path for the row involved
     } 
}

3 Comments

i try appending the in dispatchQueue.main.async but still gives me random tableview cells
Because: Enqueue them in order 1, 2, 3, 4 and they could come back in order 3, 1, 4, 2.
Like download file 1, 2, 3 and 4. Maybe 3 is the smallest file, and that is returned first after 1 second. File 2 is on some server that's a bit slow today, or the network is glitchy, and that comes back after 30 seconds.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.