Ivan Cantarino
Ivan Cantarino

Reputation: 3246

Update tableView data after retrieving new data from CloudKit

I'm currently retrieving data from CloudKit and populating the table view successfully.

I've an instance that detects if the scroll is on the last row, and if so, it re-queueries the server to retrieve more data.

After retrieving the desired data, successfully, I'm inserting the specific items, at specific indexPath, and then I reload the table view.

What happens is that when I'm at the last row, the tableView.reloadData() makes my app to flicker the screen, but everything works properly.

After a few seconds, my debug starts printing the same error, for each cell which is:

This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.

I'm calculating the heightForRowAtIndexPath as this :

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    var cell = PostFeedTableViewCell()
    cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.PostWithImage) as! PostFeedTableViewCell
    return cell.bounds.size.height;
}

And my code to retrieve data once the user reaches the last row is the following:

func continueQuering(cursor: CKQueryCursor?) {
    if cursor != nil {
        self._loadingData = true

        let publicData = CKContainer.default().publicCloudDatabase
        let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)

        let q = CKQuery(recordType: "Posts", predicate: predicate)
        q.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]

        let qop = CKQueryOperation(query: q)
        qop.resultsLimit = 5
        qop.cursor = cursor

        qop.recordFetchedBlock = {(record) in
            print("appending")
            self.posts.append(record)
            self.tableView.insertRows(at: [IndexPath.init(row: self.posts.count - 1, section: 0)], with: .automatic)
        }

        qop.queryCompletionBlock  = { [weak self] (cursor : CKQueryCursor?, error : Error?) -> Void in
            print("********************")
            print("cursor = \(String(describing: cursor)) , error = \(String(describing: error))")
            print("********************")
            self?._Cursor = cursor 
            self?._loadingData = false
            DispatchQueue.main.async {
                print("reloading async ******")
                self?.tableView.reloadData()
            }
        }
        publicData.add(qop)
    }
}

I need some help on how to avoid the flicker screen, probably caused by the tableView.reloadData() function, and how to fix these warnings that prompt after a few seconds.

If I don't call the reloadData() method, all the rows are added but the scroll doesn't move, gets stuck at the previous last row;

Thanks.

Upvotes: 0

Views: 945

Answers (2)

Thunk
Thunk

Reputation: 4177

I believe the error stems from self.tableView.insertRows running in the completion block in a background thread. Try dispatching insertRows to the main thread:

qop.recordFetchedBlock = {(record) in
        print("appending")
        self.posts.append(record)
        DispatchQueue.main.async {
             self.tableView.insertRows(at: [IndexPath.init(row: self.posts.count - 1, section: 0)], with: .automatic)
        }
}

Upvotes: 0

Ivan Cantarino
Ivan Cantarino

Reputation: 3246

So I've managed to solve this, I'll explain how and then provide the updated code;

As I retrieve the code from the server with a background thread, I had issues updating the UI, since Apple documentation says we should update the UI only in the main thread.

After implementing the

DispatchQueue.Main.async {
    self.tableView.insertRows(at: [IndexPath.init(row: self.posts.count - 1, section: 0)], with: .automatic)
}

I still had issues, and the app crashed prompting the error reminding that the app data source should have the same objects before and after the update.

I figure it out that appending data on a background thread and then updating the UI on the Main thread simultaneously would fire that error, and even though that, my tableView scroll didn't update, if I previously had 5 rows, after inserting, lets say 10 rows, the scroll would still be stuck on the 5th one.

To solve this, I had to do the update synchronously with the tableView.beginUpdates() | tableView.endUpdates() properties, so the tableView would update at the same time as the dataSource array would be filled and updating it's own properties with the begin/end updates, instead of calling the reloadData() to update the whole tableView.

I've noticed that configuring the cell to present on the willDisplayCell() method, made the app slightly buggy, not perfectly smooth, so I've moved the configuration to the cellForRowAtIndexPath() method, and only checking in the willDisplayCell() method if it's presenting the last one, and if it is, then query the server to retrieve more data, starting on the previous cursor point.

As said, the resolving code is the following:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // Default não tem image, assim se não houver posts para download retorna a cell vazia
    var cell = PostFeedTableViewCell()

    if (posts.count == 0) { return cell } /* Não tem posts, retorna uma cell vazia */

    let post = posts[indexPath.row]

    // Instancia o reuse identifier
    if post["post_image"] != nil {
        cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.PostWithImage, for: indexPath) as! PostFeedTableViewCell
    } else {
        cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.PostWithoutImage, for: indexPath) as! PostFeedTableViewCell
    }
    configureCell(cell: cell, atIndexPath: indexPath)
    return cell
}


override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if (!_loadingData) && indexPath.row == posts.count - 1 {
        print("last row displayed")
        continueQuering(cursor: _Cursor)
    }
}

func continueQuering(cursor: CKQueryCursor?) {
    if cursor != nil {
        self._loadingData = true

        // novo query com cursor a continuar na posição anterior
        let publicData = CKContainer.default().publicCloudDatabase
        let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)

        let q = CKQuery(recordType: "Posts", predicate: predicate)
        q.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]

        let qop = CKQueryOperation(query: q)
        qop.resultsLimit = 5
        qop.cursor = cursor

        qop.recordFetchedBlock = {(record) in
            self.posts.append(record)
            print("appending...")
            DispatchQueue.main.sync {
                print("updating...")
                self.tableView.beginUpdates()
                self.tableView.insertRows(at: [IndexPath.init(row: self.posts.count - 1, section: 0)], with: .automatic)
                self.tableView.endUpdates()
            }

        }

        qop.queryCompletionBlock  = { [weak self] (cursor : CKQueryCursor?, error : Error?) -> Void in
            self?._Cursor = cursor // Actualiza o cursor se houver mais dados
            self?._loadingData = false
            print("cursor = \(cursor)")
        }
        publicData.add(qop)

    }
}

Upvotes: 1

Related Questions