0

I am using Firebase to populate a TableView in my iOS app. The first few objects are loaded but once I get to the third item in my list the app crashes with the exception:

'NSRangeException', reason: '*** __boundsFail: index 3 beyond bounds [0 .. 2]'

I know that this means that I am referring to an array at an index that it does not contain however I do not know why.

I create the TableView with a TableViewController and initialize it like so:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        print(posts.count)
        return posts.count
    }


    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let post = posts[indexPath.row]
        print(post)
        let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell

        let firstReference = storageRef.child(post.firstImageUrl)
        let secondReference = storageRef.child(post.secondImageUrl)

        cell.firstTitle.setTitle(post.firstTitle, for: .normal)
        cell.secondTitle.setTitle(post.secondTitle, for: .normal)
        cell.firstImageView.sd_setImage(with: firstReference)
        cell.secondImageView.sd_setImage(with: secondReference)

        // Configure the cell...

        return cell
    }

I believe that the first function creates an array with the number of objects in posts and that the second function assigns values to the template for the cell. The print statement in the first method prints 4 which is the correct number of objects retrieved from firebase. I assume that means an array is created with 4 objects to be displayed in the TableView. This is what is really confusing because the error states that there are only 3 objects in the array. Am I misunderstanding how the TableView is instantiated?

Here is the code that fills the TableView:

func loadMessages(){
        db.collectionGroup("userPosts")
            .addSnapshotListener { (querySnapshot, error) in

            self.posts = []

            if let e = error{
                print("An error occured trying to get documents. \(e)")
            }else{
                if let snapshotDocuments = querySnapshot?.documents{
                    for doc in snapshotDocuments{
                        let data = doc.data()
                        if let firstImage = data[K.FStore.firstImageField] as? String,
                            let firstTitle = data[K.FStore.firstTitleField] as? String,
                            let secondImage = data[K.FStore.secondImageField] as? String,
                            let secondTitle = data[K.FStore.secondTitleField] as? String{
                            let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
                            self.posts.insert(post, at: 0)
                            print("Posts: ")
                            print(self.posts.capacity)
                            DispatchQueue.main.async {
                                self.tableView.reloadData()
                            }
                        }
                    }
                }
            }
        }

The app builds and runs and displays the first few items but crashes once I scroll to the bottom of the list. Any help is greatly appreciated.

Edit:

override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
        loadMessages()
    }
David
  • 769
  • 1
  • 6
  • 28
  • There is no evidence in the code for an out-of-bounds crash. It's unrelated but replace `self.tableView.reloadData()` with `self.tableView.insertRows(at: [0,0], with: .automatic)` to get an animated insertion or move the `DispatchQueue` block to after the loop. – vadian Jun 09 '20 at 18:38
  • @vadian, Thank you for your comment is it possible that there is code I could add that would help? I assumed that this is were the ```TableView``` was created so would be the source of the problem – David Jun 09 '20 at 18:42
  • Also consider parsing the data in a background queue. Firestore returns on the main thread. – trndjc Jun 09 '20 at 20:53
  • @bsod I am not familiar with how to do that, could you point me to a resource that could help me? I would very much appreciate it. Thank you so much for all of your help. – David Jun 09 '20 at 20:56
  • Edited my answer to include background queueing. Read this first: https://stackoverflow.com/questions/19179358/concurrent-vs-serial-queues-in-gcd/53582047#53582047 – trndjc Jun 09 '20 at 21:01

2 Answers2

1

You're getting an out-of-bounds error because you're dangerously populating the datasource. You have to remember that a table view is constantly adding and removing cells as it scrolls which makes updating its datasource a sensitive task. You reload the table on each document iteration and insert a new element in the datasource at index 0. Any scrolling during an update will throw an out-of-bounds error.

Therefore, populate a temporary datasource and hand that off to the actual datasource when it's ready (and then immediately reload the table, leaving no space in between an altered datasource and an active scroll fetching from that datasource).

private var posts = [Post]()
private let q = DispatchQueue(label: "userPosts") // serial queue

private func loadMessages() {
    db.collectionGroup("userPosts").addSnapshotListener { [weak self] (snapshot, error) in
        self?.q.async { // go into the background (and in serial)
            guard let snapshot = snapshot else {
                if let error = error {
                    print(error)
                }
                return
            }
            var postsTemp = [Post]() // setup temp collection
            for doc in snapshot.documents {
                if let firstImage = doc.get(K.FStore.firstImageField) as? String,
                    let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
                    let secondImage = doc.get(K.FStore.secondImageField) as? String,
                    let secondTitle = doc.get(K.FStore.secondTitleField) as? String {
                    let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
                    postsTemp.insert(post, at: 0) // populate temp
                }
            }
            DispatchQueue.main.async { // hop back onto the main queue
                self?.posts = postsTemp // hand temp off (replace or append)
                self?.tableView.reloadData() // reload
            }
        }
    }
}

Beyond this, I would handle this in the background (Firestore returns on the main queue) and only reload the table if the datasource was modified.

trndjc
  • 11,654
  • 3
  • 38
  • 51
  • Thank you for your answer. I applied your solution, however I am still getting the same problem. I have edited my question to include my ```viewDidLoad``` just in case that it part of the issue. – David Jun 09 '20 at 20:45
  • Your problem is somewhere else then. Strip the view controller down to bare minimum and find out where you've gone wrong. But I can tell you with certainty that modifying the datasource the way you were while scrolling will throw that error. – trndjc Jun 09 '20 at 20:51
0

After some fiddling around and implementing @bsod's response I was able to get my project running. The solution was in Main.Storyboard under the Attributes inspector I had to set the content to Dynamic Prototypes.

David
  • 769
  • 1
  • 6
  • 28