UITableView refresh issue

Hello, I'm working on an app that is fetching data from Firebase and the data is loaded in a UITableView. I'm using pagination (loading 3 posts at a time). Users are allowed to delete their own post. The issue I'm having is when the user deletes the post (first from Firebase and then from the array), the tableView removes the user's post, but it also removes (from the view only) the next 2 posts. Then all I see are the next 3 posts and if I scroll up to see the previous ones, the app crashes because the indexPath.row is out of range. Below is my code. Any help is greatly appreciated:

override func viewDidLoad() {
    super.viewDidLoad()
    
    configureUI()
    fetchGlimpseData()
    configureRefreshControl()
  }

   func configureRefreshControl() {
    let refreshControl = UIRefreshControl()
    refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
    glimpseTableView?.refreshControl = refreshControl
  }
   
  private func createSpinnerFooter() -> UIView {
    let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 100))
    let spinner = UIActivityIndicatorView()
    spinner.center = footerView.center
    spinner.color = .brick
    footerView.addSubview(spinner)
    spinner.startAnimating()
     
    return footerView
  }

 @objc func handleRefresh() {
    glimpse.removeAll(keepingCapacity: false)
    self.currentKey = nil
    fetchGlimpseData()
  }

func fetchGlimpseData() {
      if lastDocument == nil {
        GLIMPSE_ALL_USERS_DATA.order(by: TIMESTAMP, descending: true).limit(to: 3)
          .getDocuments { [self] (snapshot, error) in.... //this block of code works properly

else {
        glimpseTableView.tableFooterView = createSpinnerFooter()
         
        GLIMPSE_ALL_USERS_DATA.order(by: TIMESTAMP, descending: true).start(afterDocument: lastDocument!).limit(to: 3)
          .getDocuments { [self] (snapshot, error ) in.......//this block of code works properly



   func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    let position = scrollView.contentOffset.y
    let maxOffset = scrollView.contentSize.height - scrollView.frame.size.height
    if maxOffset - position <= 50 {
      fetchGlimpseData()
    }}


func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return glimpse.count
  }
   
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let gCell = glimpseTableView.dequeueReusableCell(withIdentifier: "GlimpseCell", for: indexPath) as! GlimpseCell
     
    gCell.delegateProfilePic = self
    gCell.delegateGlimpseZoom = self
    gCell.delegateGlimpseOption = self
    gCell.configureGlimpseCell(glimpse: glimpse[indexPath.row])
    gCell.separatorInset = .init(top: 5, left: 0, bottom: 5, right: 0)
    gCell.backgroundColor = .beige
    gCell.layer.borderWidth = 1
    gCell.layer.borderColor = UIColor.gray.cgColor
    gCell.selectionStyle = .none
     
    return gCell
  }
   
  func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    return .delete
  }

  func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return false
  }

  func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    guard editingStyle == .delete else { return }
    glimpseTableView.beginUpdates()
    glimpse.remove(at: indexPath.row)
    glimpseTableView.deleteRows(at: [indexPath], with: .automatic)
    glimpseTableView.reloadData()
    glimpseTableView.endUpdates()
  }
  • Please show enough code. You are hiding the most suspicious parts with this block of code works properly. How have you checked if it works properly?

  • Hi, code has been added. Please see below.

Add a Comment

Accepted Reply

Thanks for showing your code. There are many things I do not see, but as far as I check the currently shown parts of your code, you need to keep two properties glimpse and lastDocument consistent.

When you remove all the elements from glimpse, you need to set lastDocument to nil.

    glimpse.removeAll(keepingCapacity: false)
    lastDocument = nil //<-

There may be other parts you need to fix, frankly I do not understand why the app crashes because the indexPath.row is out of range. But please try adding one line shown above and tell us what happens.

  • that did it!!!! thank you so much, I'v been scratching my head with this for 2 days. One more question (if allowed) is there other way or best way to refresh a table view without the use of UIRefreshControl or a listener? I've also added handleRefresh inside viewDidAppear and viewWillAppear, this duplicates the first batch of docs I get from Firebase. Thank you again!!!

  • Generally, you should not include two or more topics in one thread. And in this case, I do not understand what you want to achieve. You should better start a new thread and clarify what you want to do.

Add a Comment

Replies

func fetchGlimpseData() {
      if lastDocument == nil {
        GLIMPSE_ALL_USERS_DATA.order(by: TIMESTAMP, descending: true).limit(to: 3)
          .getDocuments { [self] (snapshot, error) in

          guard let last = snapshot?.documents.last else { return }
            for dictionary in snapshot!.documents {
              
            let name = dictionary[FIRST_NAME] as? String ?? ""
            let age = dictionary[AGE] as? String ?? ""
            let city = dictionary[CURRENT_CITY] as? String ?? ""
            let profileImageURL = dictionary[PROFILE_IMAGE_URL] as? String ?? ""
            let glimpseCaption = dictionary[GLIMPSE_CAPTION] as? String ?? ""
            let glimpseURL = dictionary[GLIMPSE_IMAGE_URL] as? String ?? ""
            let timestamp = dictionary[TIMESTAMP] as? Double ?? 0.0
            let documentID = dictionary.documentID
             
            let glimpseInfo = Glimpse(name: name, age: age, city: city, profileImageURL: profileImageURL, glimpseCaption: glimpseCaption, glimpseURL: glimpseURL, timestamp: Date(timeIntervalSince1970: timestamp), documentID: documentID)
               
              self.glimpse.append(glimpseInfo)
              self.glimpseTableView.reloadData()
            }
          self.lastDocument = last
        }
      } else {
        glimpseTableView.tableFooterView = createSpinnerFooter()
         
        GLIMPSE_ALL_USERS_DATA.order(by: TIMESTAMP, descending: true).start(afterDocument: lastDocument!).limit(to: 3)
          .getDocuments { [self] (snapshot, error ) in
             
            DispatchQueue.main.async {
              self.glimpseTableView.tableFooterView = nil
            }
             
            guard let last = snapshot?.documents.last else { return }

            for dictionary in snapshot!.documents {

            let name = dictionary[FIRST_NAME] as? String ?? ""
            let age = dictionary[AGE] as? String ?? ""
            let city = dictionary[CURRENT_CITY] as? String ?? ""
            let profileImageURL = dictionary[PROFILE_IMAGE_URL] as? String ?? ""
            let glimpseCaption = dictionary[GLIMPSE_CAPTION] as? String ?? ""
            let glimpseURL = dictionary[GLIMPSE_IMAGE_URL] as? String ?? ""
            let timestamp = dictionary[TIMESTAMP] as? Double ?? 0.0
            let documentID = dictionary.documentID

            let glimpseInfo = Glimpse(name: name, age: age, city: city, profileImageURL: profileImageURL, glimpseCaption: glimpseCaption, glimpseURL: glimpseURL, timestamp: Date(timeIntervalSince1970: timestamp), documentID: documentID)
              self.glimpse.append(glimpseInfo)
              self.glimpseTableView.reloadData()
            }
            self.lastDocument = last
        }}
    }


//This is what i use to delete the post

func optionsMenu(sendUser: String) {
     
    let alert = UIAlertController(title: "MyApp", message: "Options", preferredStyle: .actionSheet)
    alert.view.tintColor = .brick
     
    let deleteGlimpse = UIAlertAction(title: "Delete Post", style: .default) { (action) in
      GLIMPSE_USER_COLLECTION.delete() { err in
        if let err = err {
          print("COULD NOT REMOVE GLIMPSE...\(err.localizedDescription)")
        } else {
          print("IMG HAS BEEN DELETED")
        }}
      GLIMPSE_USER_STORAGE.delete()
      self.handleRefresh()
    }
     
    let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
    alert.addAction(deleteGlimpse)
    alert.addAction(cancelAction)
     
    if let popoverController = alert.popoverPresentationController {
      popoverController.sourceView = self.view
      popoverController.backgroundColor = .green
      popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
      popoverController.permittedArrowDirections = []
    }
    self.present(alert, animated: true, completion: nil)

  }


Your code was not properly indented, making it more difficult to read:

func fetchGlimpseData() {
    if lastDocument == nil {
        GLIMPSE_ALL_USERS_DATA.order(by: TIMESTAMP, descending: true).limit(to: 3)
            .getDocuments { [self] (snapshot, error) in
                
                guard let last = snapshot?.documents.last else { return }
                for dictionary in snapshot!.documents {
                    
                    let name = dictionary[FIRST_NAME] as? String ?? ""
                    let age = dictionary[AGE] as? String ?? ""
                    let city = dictionary[CURRENT_CITY] as? String ?? ""
                    let profileImageURL = dictionary[PROFILE_IMAGE_URL] as? String ?? ""
                    let glimpseCaption = dictionary[GLIMPSE_CAPTION] as? String ?? ""
                    let glimpseURL = dictionary[GLIMPSE_IMAGE_URL] as? String ?? ""
                    let timestamp = dictionary[TIMESTAMP] as? Double ?? 0.0
                    let documentID = dictionary.documentID
                    
                    let glimpseInfo = Glimpse(name: name, age: age, city: city, profileImageURL: profileImageURL, glimpseCaption: glimpseCaption, glimpseURL: glimpseURL, timestamp: Date(timeIntervalSince1970: timestamp), documentID: documentID)
                    
                    self.glimpse.append(glimpseInfo)
                    self.glimpseTableView.reloadData()
                }
                self.lastDocument = last
            }
    } else {
        glimpseTableView.tableFooterView = createSpinnerFooter()
        
        GLIMPSE_ALL_USERS_DATA.order(by: TIMESTAMP, descending: true).start(afterDocument: lastDocument!).limit(to: 3)
            .getDocuments { [self] (snapshot, error ) in
                
                DispatchQueue.main.async {
                    self.glimpseTableView.tableFooterView = nil
                }
                
                guard let last = snapshot?.documents.last else { return }
                
                for dictionary in snapshot!.documents {
                    
                    let name = dictionary[FIRST_NAME] as? String ?? ""
                    let age = dictionary[AGE] as? String ?? ""
                    let city = dictionary[CURRENT_CITY] as? String ?? ""
                    let profileImageURL = dictionary[PROFILE_IMAGE_URL] as? String ?? ""
                    let glimpseCaption = dictionary[GLIMPSE_CAPTION] as? String ?? ""
                    let glimpseURL = dictionary[GLIMPSE_IMAGE_URL] as? String ?? ""
                    let timestamp = dictionary[TIMESTAMP] as? Double ?? 0.0
                    let documentID = dictionary.documentID
                    
                    let glimpseInfo = Glimpse(name: name, age: age, city: city, profileImageURL: profileImageURL, glimpseCaption: glimpseCaption, glimpseURL: glimpseURL, timestamp: Date(timeIntervalSince1970: timestamp), documentID: documentID)
                    self.glimpse.append(glimpseInfo)
                    self.glimpseTableView.reloadData()
                }
                self.lastDocument = last
            }}
}

It should be useful to have some prints to see what you have exactly in dictionary. Why do you reload table inside the for loop ?

I see also that you remove all items of glimpse in handleRefresh, but you leave the tableView as is ? So when you reload, you may have inconsistencies (you should check).

  • Hello, here is a print out example of one of the post:

    GLIMPSEINFO..["glimpseCaption": diving!, "profileImageUrl": https://firebasestorage.goo000000.com:443/v0/b/APPNAME00000.appspot.com/o/profileImages%25...., "currentCity": Miami,FL, "timestamp": 1632796611, "age": 48, "firstName": Eduardo, "glimpseImageUrl": https://firebasestorage.goo000000.com:443/v0/b/APPNAME00000.appspot.com/o/glimpse%2FYzWhJlzYLkhzQXxxxxxxqffMNmw1%2FDA73DFD.....

    I use reloadData() inside the for loop because if I don't, the table view does not load the data. It has to be done after fetching the data from Firebase. Regarding glimpse.removeAll(), if I pull down to refresh the data, I'm downloading the info again and if there are any changes I need to make sure the array and firebase have the same data.

Add a Comment

Thanks for showing your code. There are many things I do not see, but as far as I check the currently shown parts of your code, you need to keep two properties glimpse and lastDocument consistent.

When you remove all the elements from glimpse, you need to set lastDocument to nil.

    glimpse.removeAll(keepingCapacity: false)
    lastDocument = nil //<-

There may be other parts you need to fix, frankly I do not understand why the app crashes because the indexPath.row is out of range. But please try adding one line shown above and tell us what happens.

  • that did it!!!! thank you so much, I'v been scratching my head with this for 2 days. One more question (if allowed) is there other way or best way to refresh a table view without the use of UIRefreshControl or a listener? I've also added handleRefresh inside viewDidAppear and viewWillAppear, this duplicates the first batch of docs I get from Firebase. Thank you again!!!

  • Generally, you should not include two or more topics in one thread. And in this case, I do not understand what you want to achieve. You should better start a new thread and clarify what you want to do.

Add a Comment