addNotificationBlock { RealmCollectionChange } crashes in UITableView - ios

I'm using Realm for Swift and loading the data into a UITableView. There are roughly 200 data objects that are being gradually downloaded as I enter the screen, so there is a lot of insertion into the UITableView happening in my tests after the tableview has been displayed. I'm using the Realm example to addNotificationBlock with RealmCollectionChange as closely as I can and I'm getting two separate crashes that happen occasionally during this process.
*** Terminating app due to uncaught exception 'RLMException', reason: 'Can only add notification blocks from within runloops.'
This crash is occurring even though I make a point of pulling all the data from the main thread within my ViewController class.
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3512.30.14/UITableView.m:1720
**** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (27) must be equal to the number of rows contained in that section before the update (17), plus or minus the number of rows inserted or deleted from that section (9 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
This crash only started occurring after I replaced
tableView.reloadData()
with
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.deleteRowsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.reloadRowsAtIndexPaths(modifications.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.endUpdates()
in the .Update() section of my addNotificationBlock
Is there something I'm missing about Realm that is causing this? I suspect it's due to me not fully understand the inner mechanisms of this library.
Here's my code for reference:
self.exhibits = DataManager.getAllExhibitsSorted("id")
token = self.exhibits?.addNotificationBlock { (changes: RealmCollectionChange) in
switch changes {
case .Initial(_):
self.exhibits = DataManager.getAllExhibitsSorted("id")
self.exhibitListTableView.reloadData()
break
case .Update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
self.exhibitListTableView.beginUpdates()
self.exhibitListTableView.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
self.exhibitListTableView.deleteRowsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
self.exhibitListTableView.reloadRowsAtIndexPaths(modifications.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
self.exhibitListTableView.endUpdates()
break
case .Error:
NSLog("Error in notificationBlock")
break
}
}

It sounds like you might be overcomplicating things a little here.
Realm Results objects are live and auto-updating. Meaning changes made to their underlying objects are updated automatically on the next iteration of the main run loop, so there's no need to perform a manual re-fetch on them. In your code there, you're re-assinging self.exhibits in the .Initial change notification, after the token has been generated, which may be causing some of your issues here. If you delete that line, it should just continue to work.
I'd recommend going through your code, and making sure that self.exhibits is only being assigned once, and that the change notification method is applied to just that one.
Let me know if that doesn't fix it.

Related

How to append row in tableview swift?

I'm adding data in my model and model is assigned to tableview to reload data. But every time reloading is not looking good. so I want just last element that was added in model, should be appended in already exist tableview. Tried so many ways but getting crash when my tableview is empty.
let lastSectionIndex = self.isGroupChat ? self.objGroupChatList!.count-1 : self.objSingleChatList!.count-1
var lastRow = 0
if self.isGroupChat {
lastRow = (self.objGroupChatList?[lastSectionIndex].count ?? 1)
} else {
lastRow = (self.objSingleChatList?[lastSectionIndex].count ?? 1)
}
let IndexPathOfLastRow = IndexPath(row: lastRow-1, section: lastSectionIndex)
self.tableView.beginUpdates()
self.tableView.insertRows(at: [IndexPathOfLastRow], with: UITableViewRowAnimation.none)
self.tableView.endUpdates()
This is crashing with error:
Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of sections. The number of sections contained in the table
view after the update (1) must be equal to the number of sections
contained in the table view before the update (0), plus or minus the
number of sections inserted or deleted (0 inserted, 0 deleted).'
You should use insertSections for new sections. insertRows only works for existing sections.
You need to do something like,
let section = 0 //get your section here...
dataSource[section].append("five")
let row = dataSource[section].count - 1
tableView.insertRows(at: [IndexPath(row: row, section: section)], with: .none)
This is just an example of how you can get that working. Fill the gaps as per your code.

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.

App crashes after updating CoreData model that is being displayed in a UITableView

I have a strange bug that is extremely rare but causes the app to crash. I can't reproduce it but I finally found a crash report documenting this.
(I posted the stack trace below. I used a screenshot, as the quotes function here messed up the formatting. That would be unreadable)
So the problem begins after tapping a button, which calls the method closeButtonTapped.
This method is supposed to fade out a popup-view (called ExtendBitPopupView) and save the text the user entered (details attribute of one of my data models).
That's the closeButtonTapped method:
func closeButtonTapped(tap: UITapGestureRecognizer) {
fadeOut { // fadeOut(completion:) just fades out the UI
if self.infoTextView.text != "Enter details..." {
self.entry.info = self.infoTextView.text
self.appDelegate.saveContext()
}
}
}
So it takes the text the user entered and saves it as entry.info to the database.
Now, a little bit of context: The ExtendBitPopupView is a popup that fades in above a UITableView that displays all entry objects there are in the database. It's using a NSFetchedResultsController to manage the data. The table does not show the entry.info attribute. That is only visible inside the ExtendBitPopupView
According to the stack trace, the app crashes while calling the controllerDidChange method. I guess it calls this method because an entry has changed.
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .update: // I guess this case is being used
let cell = tableView.cellForRow(at: indexPath!) as! BitCell
let entry = fetchedResultsController.object(at: indexPath!)
cell.configure(entry: entry)
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
}
}
Line 224 is mentioned in the crash log. It's this line:
let cell = tableView.cellForRow(at: indexPath!) as! BitCell
I can't figure out why the app could crash at this moment. Also, it does work correctly 99% of the time.
My only observation is that when it happens, I typed in quite a lot of text. But I'm not sure about this, as it only happened like 3-4 times so far.
Does anyone have any ideas? I don't know what I can try and I don't know how to reproduce this bug.
If you need any more code, let me know. I just posted the code that is mentioned in the crash log.
Thanks in advance!
indexPath is the index BEFORE the deletes and inserts are applied; newIndexPath is the index AFTER the deletes and inserts are applied.
For updates you don't care where it was BEFORE the inserts and delete - only after - so use newIndexPath not indexPath. This will fix crashes that can happen when you an update and insert (or update and delete) at the same time.
For move the delegate is saying where it moved from BEFORE the inserts and where it should be inserted AFTER the inserts and deletes. This can be challenging when you have a move and insert (or move and delete). I fixed this by saving all the changes from controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: into three different indexPath arrays: insert, delete and update. When you get a move add an entry for it in both the insert array and in the delete array. In controllerDidChangeContent: sort the delete array descending and the insert array ascending. Then apply the changes - first delete, then insert, then update. This will fix crashes that can happens when you have a move and insert (or move and delete) at the same time.
It is the same principle for sections. Save the sections changes in arrays, and then apply the changes in order: deletes (descending), sectionDelete (descending), sectionInserts (ascending), inserts(ascending), updates (any order). Sections can't move or be updated.

Nightmare with performBatchUpdates crash

I am facing a nightmare of a crash during performBatchUpdates on a collection view.
The problem is basically this: I have a lot of images on a directory on a server. I want to show the thumbnails of those files on a collection view. But the thumbnail have to be downloaded from the server asynchronously. As they arrive they will be inserted on the collection view using something like this:
dispatch_async(dispatch_get_main_queue(),
^{
[self.collectionView performBatchUpdates:^{
if (removedIndexes && [removedIndexes count] > 0) {
[self.collectionView deleteItemsAtIndexPaths:removedIndexes];
}
if (changedIndexes && [changedIndexes count] > 0) {
[self.collectionView reloadItemsAtIndexPaths:changedIndexes];
}
if (insertedIndexes && [insertedIndexes count] > 0) {
[self.collectionView insertItemsAtIndexPaths:insertedIndexes];
}
} completion:nil];
});
the problem is this (I think). Suppose that at time = 0, the collection view has 10 items. I then add 100 more files to the server. The application sees the new files and start downloading the thumbnails. As the thumbnails download they will be inserted on the collection view. But because the downloads can take different times and this download operation is asynchronous, at one point iOS will lost track of how many elements the collection has and the whole thing will crash with this catastrophic infamous message.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of items in section 0. The number of items contained in an
existing section after the update (213) must be equal to the number of
items contained in that section before the update (154), plus or minus
the number of items inserted or deleted from that section (40
inserted, 0 deleted) and plus or minus the number of items moved into
or out of that section (0 moved in, 0 moved out).'
The proof I have something fishy is going on is that if I print the count of items on the data set I see exactly 213. So, the dataset matches the correct number and the message is nonsense.
I have had this problem before, here but that was an iOS 7 project. Somehow the problem returned now on iOS 8 and the solutions there are not working and now the dataset IS IN SYNC.
It sounds like you need to do a bit extra work with batching which images have appeared for each animation group. From dealing with crashes like this before, the way performBatchUpdates works is
Before invoking your block, it double checks all the item counts and saves them by calling numberOfItemsInSection (this is the 154 in your error message).
It runs the block, tracking the inserts/deletes, and calculates what the final number of items should be based on the insertions and deletions.
After the block is run, it double checks the counts it calculated to the actual counts when it asks your dataSource numberOfItemsInSection (this is the 213 number). If it doesn't match, it will crash.
Based on your variables insertedIndexes and changedIndexes, you're pre-calculating which things need to show up based on the download response from server, and then running the batch. However I'm guessing your numberOfItemsInSection method is always just returning the 'true' count of items.
So if a download completes during step 2, when it performs the sanity check in '3', your numbers won't line up anymore.
Easiest solution: Wait until all files have downloaded, then do a single batchUpdates. Probably not the best user experience but it avoids this issue.
Harder solution: Perform batches as needed, and track which items have already shown up / are currently animating separately from the total number of items. Something like:
BOOL _performingAnimation;
NSInteger _finalItemCount;
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _finalItemCount;
}
- (void)somethingDidFinishDownloading {
if (_performingAnimation) {
return;
}
// Calculate changes.
dispatch_async(dispatch_get_main_queue(),
^{
_performingAnimation = YES;
[self.collectionView performBatchUpdates:^{
if (removedIndexes && [removedIndexes count] > 0) {
[self.collectionView deleteItemsAtIndexPaths:removedIndexes];
}
if (changedIndexes && [changedIndexes count] > 0) {
[self.collectionView reloadItemsAtIndexPaths:changedIndexes];
}
if (insertedIndexes && [insertedIndexes count] > 0) {
[self.collectionView insertItemsAtIndexPaths:insertedIndexes];
}
_finalItemCount += (insertedIndexes.count - removedIndexes.count);
} completion:^{
_performingAnimation = NO;
}];
});
}
The only thing to solve after that would be to make sure you run one final check for leftover items if the last item to download finished during an animation (maybe have a method performFinalAnimationIfNeeded that you run in the completion block)
I think the problem is caused by the indexes.
Key:
For updated and deleted items, the indexes have to be the indexes of original items.
For inserted items, the indexes have to be the indexes of final items.
Here is a demo code with comments:
class CollectionViewController: UICollectionViewController {
var items: [String]!
let before = ["To Be Deleted 1", "To Be Updated 1", "To Be Updated 2", "To Be Deleted 2", "Stay"]
let after = ["Updated 1", "Updated 2", "Added 1", "Stay", "Added 2"]
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Refresh", style: .Plain, target: self, action: #selector(CollectionViewController.onRefresh(_:)))
items = before
}
func onRefresh(_: AnyObject) {
items = after
collectionView?.performBatchUpdates({
self.collectionView?.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 3, inSection: 0), ])
// Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path
// self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0), ])
// NOTE: Have to be the indexes of original list
self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0), NSIndexPath(forRow: 2, inSection: 0), ])
// Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 4 into section 0, but there are only 4 items in section 0 after the update'
// self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0), NSIndexPath(forRow: 5, inSection: 0), ])
// NOTE: Have to be index of final list
self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 0), NSIndexPath(forRow: 4, inSection: 0), ])
}, completion: nil)
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)
let label = cell.viewWithTag(100) as! UILabel
label.text = items[indexPath.row]
return cell
}
}
For anyone having a similar issue, let me quote the documentation on UICollectionView:
If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call performBatchUpdates(_:completion:).
I was originally referencing an array of a separate model object, but decided to keep a local copy of the array within the view controller and update the array within performBatchUpdates(_:completion:).
Problem was solved.
This may be happening because you do need to also make sure with collectionViews to delete and insert sections. when you try to insert an item in a section that doesn't exist you will get this crash.
Preform Batch updates doesn't know that you meant to add section X+1 when you insert an item at X+1, X. without you already having added that section in.

Reload Table view cell with animation (Swift)

Is there a way to reload specific UITableView cells with multiple sections with animations?
I've been using:
self.TheTableView.reloadSections(NSIndexSet(index: 1), withRowAnimation: UITableViewRowAnimation.Right)
This animates all the cells in the section though. The other option is to use:
self.TheTableView.reloadRowsAtIndexPaths(<#indexPaths: [AnyObject]#>,
withRowAnimation: <#UITableViewRowAnimation#>)
EDIT:
After using reloadRowsAtIndexPaths
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (4), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Trying to find a smooth way of reloading a tableview by appending objects into an array.
Calling TheTableView.reloadData() before reloadRowsAtIndexPaths works, but the animation is glitchy. Is there another approach?
Why not just reload the cells directly? The indexPath contains the section and row so just keep track of which section you want to reload and fill the indexes in that array.
var indexPath1 = NSIndexPath(forRow: 1, inSection: 1)
var indexPath2 = NSIndexPath(forRow: 1, inSection: 2)
self.tableView.reloadRowsAtIndexPaths([indexPath1, indexPath2], withRowAnimation: UITableViewRowAnimation.Automatic)
Based on your comment you are looking to change your array and have the tableView animate in the changes. If that's the case you should consider using beginUpdates() and endUpdates() for UITableViews or even an NSFetchedResultsController so it handles all of the update animations cleanly.
self.tableView.beginUpdates()
// Insert or delete rows
self.tableView.endUpdates()
I'd recommend using NSFetchedResultsController if you're using Core Data as it simplifies this entire process. Otherwise you have to handle the changes yourself. In other words, if your array changes you need to manually remove or insert rows in the tableview using beginUpdates() and endUpdates().
If you aren't using Core Data, study this to grasp how it's all handled.
In Swift 3.0
We can reload particular row of particular section in tableview
let indexPath = IndexPath(item: 2, section: 0)
self.tableView.reloadRows(at: [indexPath], with: .automatic)
If you want to reload single section in swift 3.0 you can do this:
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
the define IndexSet in official document:
Manages a Set of integer values, which are commonly used as an index
type in Cocoa API. The range of valid integer values is
in (0, INT_MAX-1). Anything outside this range is an error.
so IndexSet is Set about index, just putting some Int values in [ ]
// for reload one section
tableView.reloadSections([section], with: .automatic)
// or using like this
tableView.reloadSections(IndexSet(integer: section), with: .automatic)
// for reload multi sections
tableView.reloadSections([1, 2, 3, section], with: .automatic)

Resources