Realm notification on RLMArray not letting me update tables - objective-c

I am currently trying to update my table view when a new chat message is sent. I set a notification block on my collection to notify the table view to begin updating but I keep getting an error saying there were 0 insertions:
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 (8)
must be equal to the number of rows contained in that section before the update (7),
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).'
Here is my notification code:
- (void)setUpRealmNotifications {
// Observe RLMResults Notifications
__weak typeof(self) weakSelf = self;
self.notificationToken = [[TERMessage objectsWhere:#"conversationID == %#", self.model.serverID] addNotificationBlock:^(RLMResults<TERMessage *> *results, RLMCollectionChange *change, NSError *error) {
if (error) {
NSLog(#"Failed to open Realm on background worker: %#", error);
return;
}
UITableView *tableView = weakSelf.tableView;
// Initial run of the query will pass nil for the change information
if (!change) {
[tableView reloadData];
return;
}
// Query results have changed, so apply them to the UITableView
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[change deletionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:[change insertionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView reloadRowsAtIndexPaths:[change modificationsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
}];
}

From the looks of it, the change notification is coming down and updating the UITableView correctly, but it would seem that your UITableView data source isn't matching the same changes.
To ensure both the results of the change notification and the UITableView data source method in charge of managing the cells doesn't fall out of sync, I'd recommend keeping a single RLMResults instance around that both the change notification and table data source refer to.
#property (nonatomic, strong) RLMResults *items;
#property (nonatomic, strong) RLMNotificationToken *token;
// ---
self.items = [TERMessage objectsWhere:#"conversationID == %#", self.model.serverID];
self.token = [self.items addNotificationBlock:^...];
// ---
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TERMessage *message = self.items[indexPath.row];
UITableViewCell *cell = ...; //configure cell
return cell;
}

Related

How to perform the batch updates on table View in ios 10 and below in objective c

I have been using the core data for chat application and updating the chat table in delegate methods of NSfetchResultController controllerDidChangeContent
NSMutableArray<NSOperation *> *blockOpetations;
blockOpetations = [NSMutableArray new];
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.chatTable beginUpdates];
}
-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch (type) {
case NSFetchedResultsChangeInsert:{
[blockOpetations addObject:[NSBlockOperation blockOperationWithBlock: ^{
[self.chatTable insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationBottom];
}]];
}
break;
default:
break;
}
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (#available(iOS 11.0, *))
{
[self.chatTable performBatchUpdates:^
{
for(NSOperation *operation in blockOpetations)
{
[operation start];
}
} completion:^(BOOL finished)
{
NSInteger lastItem = [self.fetchedResultsController sections]
[0].numberOfObjects - 1;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:lastItem
inSection:0];
[self.chatTable scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}];
} else {
}
[self.chatTable endUpdates];
}
I am performing the batch updates on table view for iOS 11 and higher.
I want to perform the batch updates on table in iOS 10 and lower version , since batchupdate is not available below iOS 10.
I want to to know how to perform such thing in lower version.
I have taken the "blockOperation" an array of NSOperation , because if multiple message comes at the same time so table view updates the newly inserted rows serially without crash.
If I may have not taken the BlockOperation then some time the error comes
:
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 (20) must be equal to the number of rows contained in that section before the update (10), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
The code written under method named "ControllerDidChangeContent", want to implement same task in iOS 10 and lower
Before iOS 11, this is done with
self.chatTable.beginUpdates()
// do stuff here
self.chatTable.endUpdates()

Assertion failure when trying to delete a row from UITableView

I got an assertion failure when I try to delete a row from a UITableView using UITableViewCellEditingStyleDelete. Hope someone could tell me what is going wrong in my code.
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2372/UITableView.m:1070
2013-01-30 14:19:21.450 MyApp[48313:c07] *** 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 (1) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
*** First throw call stack:
(0x28ee012 0x1d3fe7e 0x28ede78 0x19fef35 0xf51b8d 0xf5df95 0xf5dfc3 0xcfe9 0xf6c384 0x10a9b1b 0x1d53705 0xeb3920 0xeb38b8 0xf74671 0xf74bcf 0xf746a6 0x1144f95 0x1d53705 0xeb3920 0xeb38b8 0xf74671 0xf74bcf 0xf73d38 0xee333f 0xee3552 0xec13aa 0xeb2cf8 0x2d1adf9 0x2d1aad0 0x2863bf5 0x2863962 0x2894bb6 0x2893f44 0x2893e1b 0x2d197e3 0x2d19668 0xeb065c 0x24fd 0x2435)
libc++abi.dylib: terminate called throwing an exception
(lldb)
Here is my code:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [records count];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// Delete record from the sqlite database
NSNumber *recordDBID = [records objectAtIndex:indexPath.row];
Record *myRecord = [[Record alloc] initWithDBID:recordDBID database:UIAppDelegate.formManager.connection];
BOOL valueX = [myRecord deleteRecordInDb:recordDBID];
[myRecord release];
// Delete record from array
NSMutableArray *aNewArray = [[NSMutableArray alloc] initWithArray:records];
[aNewArray removeObjectAtIndex:indexPath.row];
records = aNewArray;
[aNewArray release];
// Delete the record from the table
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
[tableView reloadData];
}
I think there's a chance that you may be overcomplicating the removal of the table view row.
You do not need to call [tableView beginUpdates] and [tableView endUpdates] if you are only going to be removing a single row between those two calls, so you can lose them. Begin and end updates are only required if you are going to be carrying out multiple insert or delete actions at the same time.
Secondly, calling [tableView reloadData] at this point is overkill as the table view will automatically request the information it needs as part of the delete action using the assigned delegate/datasource. You can reduce the last part of the code to this:
// Delete the record from the table
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
It may be that the current row removal code (in combination with the wider code base) is confusing the system.
If the above does not help we'll need to see all code where the records array is being modified. In this event I'll update my answer.
In my case I have typos: it was indexPath instead newIndexPath in insertRowsAtIndexPaths
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
{
switch type
{
case .Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Bottom)
case .Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .None)
case .Update:
self.tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Move:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .None)
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Bottom)
}
}

Delay when refreshing UITableView custom cell

I have a UITableView with a custom cell which i fill (with array infoservices) after parsing the xml data.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
ApplicationCell *cell = (ApplicationCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
[self.cellNib instantiateWithOwner:self options:nil];
cell = tmpCell;
self.tmpCell = nil;
}
infoService *e = [self.infoservices objectAtIndex:indexPath.row];
cell.name = [e infoName];
NSString *infodetails = [e infoDetails];
if ( infodetails == nil ) {
cell.details = #"Loading...";
[self startInfoDownload:e forIndexPath:indexPath];
NSLog(#"Loading...");
} else {
cell.details = infodetails;
NSLog(#"Show info detail: %#", infodetails );
}
return cell;
}
- (void)infoDidFinishLoading:(NSIndexPath *)indexPath
{
infoDownloader *infoserv = [imageDownloadsInProgress objectForKey:indexPath];
if (infoserv != nil)
{
[infoservices replaceObjectAtIndex:[indexPath row] withObject:infoserv.appRecord];
NSIndexPath *a = [NSIndexPath indexPathForRow:indexPath.row inSection:0]; // I wanted to update this cell specifically
ApplicationCell *cell = (ApplicationCell *)[self.tableView cellForRowAtIndexPath:a];
cell.details = [[infoservices objectAtIndex:[indexPath row]] infoDetails];
NSLog(#"Updating=%#", [[infoservices objectAtIndex:[indexPath row]] infoDetails]);
}
}
For each cell i'm using NSURLConnection sendAsynchronousRequest to retrieve and parse xml data from object infoDownloader with
- (void)startDownload
for each individual cell.
After data has been successfully parsed delegate method from infoDownloader is called
- (void)infoDidFinishLoading:(NSIndexPath *)indexPath
The problem is that, while the
- (void)infoDidFinishLoading:(NSIndexPath *)indexPath
gets called after parsing each cell and i can see the
NSLog(#"Updating=%#", [[infoservices objectAtIndex:[indexPath row]] infoDetails]);
in the debugger with the correct details, the cell does not get refreshed immediately but after 6 or 7 seconds. Also cellForRowAtIndexPath does not get called from
- (void)infoDidFinishLoading:(NSIndexPath *)indexPath
for some reason because there is not debug output after the infoDidFinishLoading. Also i don't understand how the cell.details gets actually refreshed since cellForRowAtIndexPath isn't called again.
I've tried to setup this function using Apple's LazyTableImages loading example, which i have used successful, but i don't know what's going wrong.
You may need to call reloadRowsAtIndexPaths once the data has been loaded. What could be happening is that the cell data has been loaded but the cell drawing is not updated.
Also I believe your code can request the same data multiple times because if [e infoDetails] is nil a request is made, but the cell could be requested multiple times before the data is loaded so [self startInfoDownload:e forIndexPath:indexPath] would be called multiple times, downloading the same data. You should look into keeping track of which rows you have requested data for.
Check out this code for some ideas on how to solve this: https://github.com/kgn/Spectttator/blob/master/SpectttatorTest-iOS/RootViewController.m#L123
Any changes affecting UI must be performed on main thread.
Refreshing tableView, by changing cells details, realoading tabe view, realoding rows... are changes affecting UI.
In particular, you should perform your changes in the main thread using :
dispatch_async(dispatch_get_main_queue(), ^{
cell.details = [[infoservices objectAtIndex:[indexPath row]] infoDetails];
}
or prior to iOS 4 :
cell performSelectorOnMainThread:#selector(setDetails:) withObject:[[infoservices objectAtIndex:[indexPath row]] infoDetails];

Assertion failure in -[UITableView _endCellAnimationsWithContext:]

Hopefully this will be a quick fix. I have been trying to figure out the error that i keep getting. The error is listed below and the appdelagate is below that.
Any help is appreciated.
Thanks
2012-04-12 21:11:52.669 Chanda[75100:f803] --- Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1914.84/UITableView.m:1037
2012-04-12 21:11:52.671 Chanda[75100:f803] --- 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 (2) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
#import "AppDelegate.h"
#implementation AppDelegate
#synthesize window = _window;
#synthesize databaseName,databasePath;
- (BOOL)application: (UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions {
self.databaseName = #"Customers.db";
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDir = [documentPaths objectAtIndex:0];
self.databasePath = [documentDir stringByAppendingPathComponent:self.databaseName];
[self createAndCheckDatabase];
return YES;
}
- (void)createAndCheckDatabase {
BOOL success;
NSFileManager *fileManager = [NSFileManager defaultManager];
success = [fileManager fileExistsAtPath:databasePath];
if (success) return;
NSString *databasePathFromApp = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:self.databaseName];
[fileManager copyItemAtPath:databasePathFromApp toPath:databasePath error:nil];
}
#end
I don't see the reason for you to show us this part of code. Your error must be connected to this part in your code I assume
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
Probably you are making a mistake in one of these data source methods. Currently it's impossible to say what exactly is wrong but I assume it could be something like: You are telling the table view in the numberOfRowsInSection you would like to have n rows reserved and setup and in the cellForRowAtIndexPath you then only handle n - 1 rows for example.
Sorry that this answer can't be as precise as it should be. If you show us your implementation of your data source it would be much easier to tell what's going on.
Like Sun Tzu said: it's best to win without fighting. In my case whenever I see this kind of error message (ie discrepancy between rows added deleted etc).. I don't even debug anything.. I simply avoid making that extra call where I reload the rows etc.. that's 99% of the cases where this error happens.
This is a common scenario where this bug happens: I have a UINavigationController and it has a UITableView, when I click on a row it pushes a new UITableView and so on. This error always happens to me when I pop the last UITableview and go back to the UITableView before it, at this point I make an unnecessary call to the loadIt function which basically inserts the rows and relaods the UITableView.
The reason this happens is because I erroneously place my loadIt function in viewDidAppear:animated rather than viewDidLoad. viewDidAppear:animated is called every time the UITableView is displayed, viewDidLoad is only called once.
When removing rows, remember that it also checks sections when updating, in:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)theTableView
If you want to remove a row that is the last item in a section you need to remove the whole section instead (otherwise it might get section count wrong and throw this exception).
Don't forget to update your array which determines numberOfRowsInSection. It needs to be updated before you animate and remove
We check if number of rows in section is 1 because we will have to delete the entire section.
Do correct me if anyone can make this answer clearer.
[self.tableView beginUpdates];
if ([tableView numberOfRowsInSection:indexPath.section] == 1) {
[tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
} else {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
[self.tableView endUpdates];
I put each section elements in separated arrays. Then put them into another array( arrayWithArray). My solution here for this problem:
[quarantineMessages removeObject : message];
[_tableView beginUpdates];
if([[arrayWithArray objectAtIndex: indPath.section] count] > 1)
{
[_tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indPath] withRowAnimation:UITableViewRowAnimationBottom];
}
else
{
[_tableView deleteSections:[NSIndexSet indexSetWithIndex:indPath.section]
withRowAnimation:UITableViewRowAnimationFade];
}
[_tableView endUpdates];
I had the same error.
I was using the following lines
UINib *myCustomCellNib = [UINib nibWithNibName:#"CustomNib" bundle:nil];
[tableView registerNib:myCustomCellNib forCellReuseIdentifier:#"CustomNib"];
to register the nib in the viewDidLoad method, since I had a different nib that was also associated with the same class. Hence, the line
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"GBFBLoadingCell"];
was returning nil unless I registered the nib in the viewDidLoad.
My problem was that I forgot to set the identifier in the attributes inspector for my file "CustomNib.xib" and "CustomNib~iphone.xib". (Or more precisely, that I forgot to press enter after typing the identifier in the attribute inspector in XCode, so that the new name failed to save.)
Hope this helps.
If you're using an NSFetchedResultsController like me and updating data in a background thread, don't forget to begin and end updates in the delegate:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
I Had the same error , which when trying with [tableView reloadData] was working fine .
The error was actually in the line
[TabView insertRowsAtIndexPaths:indexPathsArray withRowAnimation:UITableViewRowAnimationRight];
When i tried to check the indexPath values , they weren't correct as required .
I fixed it by changing values in indexPathsArray .
Hope this helps .
Its could be one of UITableViewDataSource protocol methods
For
- tableView:numberOfRowsInSection:
it should return an integer equal to the sum or result of
-insertRowsAtIndexPaths:withRowAnimation: and/or -deleteRowsAtIndexPaths:withRowAnimation:
For
- numberOfSectionsInTableView:
it should return an integer equal to the sum or result of
-insertRowsAtIndexPaths:withRowAnimation: and/or
-deleteSections:withRowAnimation:
I had the same problem with a Core Data base. If your using many FRC, you just need to reload the tableview inside each condition in numberOfSectionsInTableView.
I just had this happen to me while using Swift and a FRC backed data store to manage information. Adding a simple check to the delete operation to evaluate the current indexPath.section allowed me to avoid an extraneous call. I think I understand why this problem occurs... Basically I load a message into the top row whenever my dataset is empty. This creates an off by one issue as there is a faux row.
My Solution
... delete my entity, save the datastore and call reloadData on tableView
//next I add this simple check to avoid calling deleteRows when the system (wrongly) determines that there is no section.
if indexPath.section > 0 {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .None)
}
Simply check that you call [yourTableView reloadData]; after modify array of values.

NSManagedObjectContext: autoupdate or not?

I need to understand something about NSManagedObjectContext update.
I have a UISplitView with a UITableViewController on the RootView and a UIViewController on the Detail View. When I tap in a row with data, I load some data into labels and a UITextView where I can update that field:
- (void)textViewDidEndEditing:(UITextView *)textView {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
[[listOfAdventures objectAtIndex:indexPath.row] setAdventureDescription:textView.text];
}
Ok. This works correctly, the description is updated.
Also, someone might wants to delete a row:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"playerPlaysAdventure.adventureName==%#",[[listOfAdventures objectAtIndex:indexPath.row] adventureName]];
NSArray *results = [[AdventureFetcher sharedInstance] fetchManagedObjectsForEntity:#"Player" withPredicate:predicate withDescriptor:#"playerName"];
[moc deleteObject:[listOfAdventures objectAtIndex:indexPath.row]];
for ( Player *player in results ) {
[moc deleteObject:player];
}
[listOfAdventures removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
[self clearDetailViewContent];
NSError *error = nil;
if ( ![moc save:&error] ) {
NSLog( #"Errore nella cancellazione del contesto!" );
abort();
}
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
So here's my problem: if I comment the rows about the saving of my MOC, the adventure is only momentarily deleted. If you quit the app and the reopen it, the object is still there. This doesn't happen with the update of a field. I'd like to know why and if I should save moc also in textViewDidFinishEditing method.
Thank you in advance.
It's the difference between changing an attribute of an object and adding or removing an entire object in the object graph.
In the first block, you change an attribute of an existing object which saves automatically unless you run an undo. This is because the object already exist in the object graph and no other objects have to be altered to make the change.
In the second block, you are removing an entire object and potentially altering the object graph itself by changing the relationships between objects. That change will not be committed until an implicit save because potentially it can trigger a cascade of changes throughout a large number of objects.