Reloading data from a tableView within a collectionViewCell - ios

I have a tableView inside a collectionViewCell and I get an error when I try to reload the data.
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.6.21/UITableView.m:1610
I tried using Dispatch.main.async and it seems to get rid of the problem. The only thing is that it doesn't reload the data and nothing changes in the tableView
func cleanItems(completion: #escaping (Bool) -> Void) {
Dispatch.main.async {
cell.tableView.beginUpdates()
// Make changes in the Data Source
cell.tableView.deleteRows(at: selectedItems, with: .fade)
cell.tableView.endUpdates()
// Reloading sections accordingly depending on what the user has deleted
// Do I need to reload data here again? It used to work without Dispatch, but it wasn't stable
cell.tableView.reloadData()
// Updating items with if statements to reload data in Firebase
completion(true)
}
}
This doesn't reload the data at all and nothing seems to change. The good thing is that I don't get a random crash, which was the case before implementing Dispatch.main.async
I've retrieved the numberOfRows in each section to see how many rows there are after ending updates.
print(cell.tableView.numberOfRows(inSection: 1))
and I get the same number of rows that are in the current view.
This is crucial, because if the tableView sections are all zero, the collectionViewCell should disappear. And we never get here in the completion block as it says that the numberOfRows has never changed. Leaving us with a non updated tableView.

I solved this by moving Dispatch.main.async outside the function call.
Dispatch.main.async {
cleanItems(completion: { (success) in
etc.
})
}

Related

Realm observe with UICollectionView - race conditions

I am using Realm as a caching layer so that whenever data is presented to the user, it is first fetched from the database and displayed to the user. Subsequently, a server request is sent to fetch the newest version of the data, sync it with the Realm database and display the changes in a UICollectionView.
The problem is that when the cached data is retrieved from the Realm database and the UICollectionView is getting updated, there is a chance that the server request for update finished before the UICollectionView loaded all the cells and since the Results list is a live collection of data, it could have been modified. Now for example, if an item was removed on the server-side, the live collection would hold one item less and therefore cause out of bounds exception.
This being said, even the code provided in official Realm documentation is not thread-safe considering the fact that the results can be changed while the UITableView is asking for each row one by one:
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person.self).filter("age > 5")
// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}
deinit {
notificationToken?.invalidate()
}
}
The only way I can think of fixing this is to create a deep copy of the results as well as synchronize the body of the observe function using Semaphore or similar to make sure the data will not get in an inconsistent state, which I consider very inefficient. (Note that tableView.endUpdates() does not mean the UITableView has reloaded all the data, however it is just dispatched to a queue and ready to be processed in async.)
I would like to hear any suggestions how to implement this in an efficient way such that the mentioned race conditions are eliminated.
You need to do all of your UI updates on the main thread. If you do this an the first sets of results updates the collection view on the main thread, when the next set of results also comes it it will be queued on the main thread so it updates after the first set is done.
Based on:
The problem is that when the cached data is retrieved from the Realm database and the UICollectionView is getting updated, there is a chance that the server request for update finished before the UICollectionView loaded all the cells and since the Results list is a live collection of data, it could have been modified.
I do not think that that will happen, since as soon as your live collection will be changed update notification will be triggered and collection will be rebuild/updated accordingly. However, as I said you in PM it was some time ago when I worked with realm.
It is quite easy to test your hypothesis: decrease speed of your simulator's internet, or make huge table, etc. I am really curious if you can actually create a problem which you think you will have.

Problems with asynchronous data, UITableView, and reloadRowsAt

I'm trying to implement a tableView that has 4 different possible prototype cells. They all inherit from base UITableViewCell class and implement its protocol.
For two of the cells there's asynchronous data fetching but one in particular has been giving me fits. The flow is as follows:
1) Dequeue reusable cell
2) Call configure
func configure(someArguments: ) {
//some checks
process(withArguments: ) { [weak self in] in
if let weakSelf = self {
weakSelf.reloadDelegate.reload(forID: id)
}
}
}
3) If the async data is in the cache, configure the cell using the image/data/stuff available and be happy
4) If the async data is NOT in the cache, fetch it, cache it, and call the completion
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
//add to cache
//call completion
}
}
}
5) If the completion is called, reload the row in question by passing the index path up to the UITableViewController and calling reloadRows(at:with:)
func reload(forID: ) {
tableView.beginUpdates()
tableView.reloadRows(at: indexPath_matching_forID with: .automatic)
tableView.endUpdates()
}
Now, my understanding is that reloadRows(at:with:) will trigger another dataSource/delegate cycle and thus result in a fresh resuable cell being dequeued, and the configure method being called again, thereby making step #3 happy (the async data will now be in the cache since we just fetched it).
Except...that's not always happening. If there are cells in my initial fetch that require reloading, it works - they get the data and display it. Sometimes, though, scrolling down to another cell that requires fetching DOES NOT get the right data...or more specifically, it doesn't trigger a reload that populates the cell with the right data. I CAN see the cache being updated with the fresh data, but it's not...showing up.
If, however, I scroll completely past the bad cell, and then scroll back up, the correct data is used. So, what the hell reloadRows?!
I've tried wrapping various things in DispatchQueue.main.async to no avail.
reloadData works, ish, but is expensive because of potentially many async requests firing on a full reload (plus it causes some excessive flickering as cells come back)
Any help would be appreciated!
Reused cells are not "fresh". Clear the cell while waiting for content.
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
// ** reset the content of the cell, clear labels etc **
//add to cache
//call completion
}
}
}

Refresh Control Issue with custom cells - Swift 4 IOS 11

I am building an iOS app and I am trying to implement a pull-down refresh control on my project. The data is fetched correctly from an API and displayed on my table. But the problem rises when I do pull down to refresh. The following situations happen:
If I pull down for a long distance from the top, and the tableview.reloadData() function is called, the cells in the non-visible portion of the table come with the default tableview cells on top of them, overlapping...
if I pull down multiple times in quick succession the same issue happens.
I believe that it is because tableview.reloadData() is called multiple times in quick succession. But why are the default cells getting dequeued on top of my custom cells? Here is the section of code in the function to handle the pulldown:
#objc func refreshFunc(){
//let offset = scrollView.contentOffset.y
if myRefreshControl.isRefreshing{
readJson { (activities) in
self.activities = activities
self.tableView.reloadData()
self.myRefreshControl.endRefreshing()
}
}
}
Any help would be appreciated. Thanks in advance
UPDATE:
Changing the code to the code below seems to remove the error, but the problem is that now I need to pull down twice in order to get the results updated on the table:
#objc func refreshFunc(){
readJson { (activities) in
self.activities = activities
}
self.tableView.reloadData()
self.myRefreshControl.endRefreshing()
}
Please note that running the reloadData on the main thread gives the same result, I still need to pull down twice to update.
Please try to give some delay before refresh table view may it resolve your problem.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayTime) { [weak self] in
self?.tableView.reloadData()
}
Hope it works
Cheers :)
DispatchQueue.main.async {
self.tableView.reloadData
}
Try this, always, if u want change UI you have to call on main thread

Reloading whole ViewController after custom Button pressed in TableView

I added 2 UIButton in my custom UITableViewCell.
When pressed something is done with the displayed object(in this case a User). Now I want the row/cell to disappear. My Idea was to reload the screen via triggering the viewDidLoad() and viewDidAppear() functions,
since i use PFQueries to obtain user data and display them in my table view.
What happens is, that other than deleting that row since my query shouldn't find the data, it just adds the same things again.
Is there a better way to solve this? I want to delete the row and redo my Query.
To delete a row from a table, you can use a function like this:
// Add this function to your ViewController
func tableDeleteRow(indexPath: NSIndexPath) {
// IMPLEMENT ME:
// first, remove the item from the data that drives the tableView.
// This is what I do. Yours will be different.
// self.tableData.removeAtIndex(indexPath.row)
// tell the table to delete the row
dispatch_async(dispatch_get_main_queue()) {
// Code
print("remove from table")
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
}
If you are looking to reload the entire table and redo the data you got from the server, do this:
// This is pseudo code.
func tableRefresh() {
// this is a pseudo code function.
// replace it with your own.
get_data_from_server() {
(data, response, error) in
// do something with data
dispatch_async(dispatch_get_main_queue()) {
print("refresh table")
self.tableView.reloadData()
}
}
}

Error updating UITableView after smaller sized JSON response?

Currently, I have my app hitting my endpoints, and getting back 10 or less items. In the event the items returned is less than 10, and the UITableView is already showing 10 items, reloadData() will cause an error because the size is not the same as it was last time. Right now, when I get my response all I do is:
tableView.beginUpdates()
self.items = items //where self.items is the array that backs the UITableView, and items are the items I got back in form of JSON from the server.
tableView.reloadData()
tableView.endUpdates()
Why will it cause error? If you are using tableView.reloadData(), you don't need to call endUpdates() and beginUpdates(). Just simply assign the items and reload tableView data.
self.items = items
tableView.reloadData()
If you are on the background thread call the above code on the main queue.
dispatch_async(dispatch_get_main_queue()) { () -> Void in
// Code runs on main queue!
}
It is not your calling reloadData that caused error. It is because your numberOfRowsInSection or numberOfSectionsInTableView didn't return right value after you update your model.
In the table view method,
(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {}
you should write as , return [self.items count];
This way, your table will know, how many items your table need to show, and reload data would not complain.

Resources