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()
  }
Answered by OOPer in 689397022

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.

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?

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)

  }


Hi, code has been added. Please see below.

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).

Accepted Answer

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.

UITableView refresh issue
 
 
Q