fatal error: Index out of range when refreshing table view
Asked Answered
P

2

5

I've this weird app crash when pulling to refresh occurs.

My code goes as it follows:

var posts: [Posts] = []

override func viewDidLoad() {
    super.viewDidLoad()

    // refreshControl -> pull to refresh handler
    let refreshControl = UIRefreshControl()
    refreshControl.addTarget(self,
                             action: #selector(Main_TVC.getData),
                             for: UIControlEvents.valueChanged)
    self.refreshControl = refreshControl

    getData()
}

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

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

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell",
                                             for: indexPath) as! PostsTableViewCell

    cell.titleLabel.text = posts[indexPath.row].postTitle
    cell.bodyLabel.text = posts[indexPath.row].postBody

    return cell
}

func getData() {
    self.posts.removeAll()

    // queries the backend and fills the data - sorry for code omission

    refresh()

}


func refresh() {
    self.tableView.reloadData()
    self.refreshControl?.endRefreshing()
}

The app runs properly and even when I pull down to refresh, everything runs perfectly, but if do a long pull down to refresh, like pulling down almost hitting the bottom of the screen, the app crashes and prompts the following error:

fatal error: Index out of range

on the line

cell.titleLabel.text = posts[indexPath.row].postTitle

If I print the post count and the indexPath as follows:

print("posts.count = (posts.count)") print("indexPath.row = (indexPath.row)")

When I'm pulling down in the normal way, it prints the correct data, but if I pull down like a long pull, through the whole screen if prompts this when it crashes

posts.count = 0

indexPath.row = 2

This kind of thing has never happen to me using the refreshControl the way I'm using here.

Hope my information is understandable, mostly on the long pull to refresh issue.

Pernik answered 19/11, 2016 at 21:41 Comment(1)
a) could refresh() be triggered more than once (I doubt it, but just confirming). b) how are you manipulating the datasource in your omitted code?Lucio
G
11

Your problem is that the first thing you do in getData is remove all of the posts from self.posts, but you do not (presumably since code is missing) reload the table view, so now the number of posts in the array (0) and the number of posts that the tableview *thinks is in the array (not zero) is different, so you get an array bounds crash.

Calling reloadData after self.posts.removeAll() would fix the issue but result in the tableview 'flashing' as it redrew empty and then redrew with the new data.

Since you haven't shown the full code for getData I can't provide the exact code you need, but it should be something like:

func getData() { 
     fetchPostsWithCompletion() {
         if let tempPosts = dataFromNetwork {
             self.posts = tempPosts
         } else {
             self.posts.removeAll()
         }
         self.refresh()        // Dispatch this on the main queue if your completion handler is not already on the main queue
    }
}

This way you don't manipulate the backing array until you have the new data.

Galligan answered 19/11, 2016 at 21:50 Comment(3)
I am wondering where precisely the error occurs. What function called by the tableView return an unexpected result? Wouldn't it first call tableView(_:, numberOfRowsInSection:) before anything else?Brazilein
I explained this in the first paragraph. After the initial call, the tableview only calls numberOfRowsInSection when it is told that the number of rows has changed, either by calling reload or delete/insert rows; if you change the source array without calling one of these methods then there is now an inconsistency between the data array and the number of rows that the tableview was told were in the data array.Galligan
@Brazilein I was also wondering this, until I realized it's probably due to the table view being "scrolled" slightly during the pull to refresh, which causes various rows to be reloaded before tableView(_:, numberOfRowsInSection: ) is ever called. This is causing the crash.Kistner
F
1

I just had the same issue. My finding is that delaying the execution of removeAll() on the posts array allows the table view to get up to date on its count.

func getData() {
   self.delayExecutionByMilliseconds(500) {
      self.posts.removeAll()
   }
   // queries the backend and fills the data - sorry for code omission

   refresh()
}

fileprivate func delayExecutionByMilliseconds(_ delay: Int, for anonFunc: @escaping () -> Void) {
    let when = DispatchTime.now() + .milliseconds(delay)
    DispatchQueue.main.asyncAfter(deadline: when, execute: anonFunc)
}
Flanker answered 5/3, 2017 at 11:40 Comment(1)
You have to be careful with delays when querying the server. I once had issues with that because the network isn't constant. Anyway, I'm glad that your solution solved your issue.Pernik

© 2022 - 2024 — McMap. All rights reserved.