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.

Ivan Cantarino
  • 3,058
  • 4
  • 34
  • 73
  • 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? – max_ Nov 19 '16 at 21:49

2 Answers2

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.

Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • 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? – Marmelador Jul 19 '17 at 18:00
  • 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. – Paulw11 Jul 19 '17 at 19:41
  • @Marmelador 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. – Eric Jan 03 '22 at 01:20
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)
}
yohannes
  • 1,022
  • 11
  • 13
  • 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. – Ivan Cantarino Mar 05 '17 at 11:55