0

I have a table view where depending on the cell class it will download an image from Firebase. I've noticed when using the app that cells with the same cell identifier will show the previous downloaded image before showing the new one. This is what I have before changing it.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if tableData[indexPath.row]["Image"] != nil {
        let cell = tableView.dequeueReusableCell(withIdentifier: "imageNotesData", for: indexPath) as! ImageNotesCell
        cell.notes.delegate = self
        cell.notes.tag = indexPath.row
        cell.notes.text = tableData[indexPath.row]["Notes"] as! String
        guard let imageFirebasePath = tableData[indexPath.row]["Image"] else {
            return cell }
        let pathReference = Storage.storage().reference(withPath: imageFirebasePath as! String)
        pathReference.getData(maxSize: 1 * 1614 * 1614) { data, error in
            if let error = error {
                print(error)
            } else {
                let image = UIImage(data: data!)
                cell.storedImage.image = image
            }
        }
        return cell
    }
    else {
        let cell = tableView.dequeueReusableCell(withIdentifier: "notesData", for: indexPath) as! NotesCell
        //let noteString = tableData[indexPath.row]["Notes"] as! String
        cell.notes.text = tableData[indexPath.row]["Notes"] as! String
        cell.notes.delegate = self
        cell.notes.tag = indexPath.row
        return cell
    }
}

Knowing that this is not a good user experience and that it looks clunky, I tried to move the pathReference.getData to where I setup the data but the view appears before my images finish downloading. I have tried to use a completion handler but I'm still having issues.

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
            getSectionData(userID: userID, city: selectedCity, completion: {(sectionString) in
            self.setupTableCellView(userID: userID, city: selectedCity, section: sectionString) { (tableData) in
                DispatchQueue.main.async(execute:  {
                    self.cityName?.text = selectedCity
                    self.changeSections.setTitle(sectionString, for: .normal)
                    self.currentSectionString = sectionString
                    self.setupTableData(tableDataHolder: tableData)
                })
            }
        })
}

func setupTableCellView(userID: String, city: String, section: String, completion: @escaping ([[String:Any]]) -> () ) {
        let databaseRef = Database.database().reference().child("Users").child(userID).child("Cities").child(city).child(section)
        var indexData = [String:Any]()
        var indexDataArray = [[String:Any]]()
        databaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
            for dataSet in snapshot.children {
                let snap = dataSet as! DataSnapshot
                //let k = snap.key
                let v = snap.value
                indexData = [:]
                for (key, value) in v as! [String: Any] {
                    //indexData[key] = value
                    if key == "Image" {
                        //let pathReference = Storage.storage().reference(withPath: value as! String)
                        print("before getImageData call")
                        self.getImageData(pathRef: value as! String, completion: {(someData) in
                            print("before assigning indexData[key]")
                            indexData[key] = someData
                            print("after assigning indexData[key]")
                        })
                    } else {
                        indexData[key] = value
                    }
                }
                indexDataArray.append(indexData)
            }
            completion(indexDataArray)
        })
    }

func getImageData(pathRef: String, completion: @escaping(UIImage) -> ()) {
    let pathReference = Storage.storage().reference(withPath: pathRef as! String)
    pathReference.getData(maxSize: 1 * 1614 * 1614, completion: { (data, error) in
        if let error = error {
            print(error)
        } else {
            let image = UIImage(data:data!)
            print("called before completion handler w/ image")
            completion(image!)
        }
    })
}

I don't know if I am approaching this the right way but I think I am. I'm also guessing that the getData call is async and that is why it will always download after showing the table view.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
LampPost
  • 856
  • 11
  • 30
  • Firebase APIs are all asynchronous when it comes to waiting for data from the server. https://medium.com/google-developers/why-are-the-firebase-apis-asynchronous-e037a6654a93 – Doug Stevenson Mar 09 '18 at 21:18
  • So I'm guessing I have to setup my data before the view loads or something. – LampPost Mar 09 '18 at 21:25
  • Very unfortunately - this is all totally, completely wrong :/ OCC is difficult .. modern "apps" (ie device-cloud systems) look really easy when you use Facebook. But, look up how many IOS programmers Facebook has. – Fattie Mar 09 '18 at 21:42

2 Answers2

1

You can't do this.

Make the request from Firebase.

Over time, you will get many replies - all the information and all the changing information.

When each new item arrives - and don't forget it may be either an addition or deletion - alter your table so that it displays all the current items.

That's OCC!

OCC is "occasionally connected computing". A similar phrase is "offline first computing". So, whenever you use any major service you use every day like Facebook, Snapchat, etc that is "OCC": everything stays in sync properly whether you do or don't have bandwidth. You know? The current major paradigm of device-cloud computing.

Fattie
  • 27,874
  • 70
  • 431
  • 719
0

Edit - See Fattie's comments about prepareForReuse()!

With reusable table cells, the cells will at first have the appearance they do by default / on the xib. Once they're "used", they have whatever data they were set to. This can result in some wonky behavior. I discovered an issue where in my "default" case from my data, I didn't do anything ecause it already matched the xib, but if the data's attributes were different, I updated the appearance. The result was that scrolling up and down really fast, some things that should have had the default appearance had the changed appearance.

One basic solution to just not show the previous image would be to show a place holder / empty image, then call your asynchronous fetch of the image. Not exactly what you want because the cell will still show up empty...

Make sure you have a local store for the images, otherwise you're going to be making a server request for images you already have as you scroll up and down!

I'd recommend in your viewDidLoad, call a method to fetch all of your images at once, then, once you have them all, in your success handler, call self.tableview.reloadData() to display it all.

Jake T.
  • 4,308
  • 2
  • 20
  • 48
  • 1
    *"This can result in some wonky behavior."* Only if your programming is wonky, Jake! What you're looking for is `prepareForReuse()` . It's the most basic thing in programmin' up a table view. Get yourself a cheap quart of whisky, have a sit-down and sort it out. Enjoy! – Fattie Mar 09 '18 at 21:30
  • Heh, pshh I meant wonky if you have no clue what you're doing and follow random tutorials you found on the internet to teach you how to do this stuff! Thanks for that tip, though! It's a new one for me, sadly... I really need to buy an up to date iOS best practice book or something... Ray Wenderlich was great, but not comprehensive... I'm still coming across and refactoring old code where EVERYTHING is in the View Controllers haha – Jake T. Mar 09 '18 at 21:32
  • Ray is an idiot. It's so utterly hopeless it's not worth mentioning. you have to `prepareForReuse()` old mate. Regarding images, it just doesn't work like that at all. You have to use a cache solution (currently, I like Haneke at the moment - there are various .... DLImageLoader was traditionally the very best. You >must< use one of these. Then each >cell< has to on its own load the appropriate image. It's tricky. – Fattie Mar 09 '18 at 21:40
  • a post to get you going ... https://stackoverflow.com/a/26355057/294884 unfortunately, that post by some drunk is very old, so useless. but it will get you pointed. bonne chance – Fattie Mar 09 '18 at 21:42