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.
Related
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;
}
Got a problem here...
My BOOL gets edited and I get success at the last NSLog, but when I close the ViewController and then go in again (update the table), the BOOL go back to the first value. That will say - something is wrong in my [context save:&error]; function.
Any ideas?
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
for (int i=0; i<[self tableView:tableView numberOfRowsInSection:0]; i++) {
AccountCell *cell = (AccountCell *)[tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
[cell setSelected:(i==indexPath.row) animated:NO];
NSManagedObject *user = [arr objectAtIndex:indexPath.row];
[user setValue:[NSNumber numberWithBool:(i==indexPath.row)] forKey:#"active"];
NSLog(#"Index: %i, Active State: %#", i,[user valueForKey:#"active"]);
NSError *error;
if (![context save:&error]) {
NSLog(#"Saving changes to context failed: %#", error);
} else {
// The changes have been persisted.
NSLog(#"Saved data success");
}
}
}
Some suggestions:
It would make much more sense to put the save statement outside the for loop.
You need to check if
your managed object context is valid (non-nil)
the context of the objects of your mysterious arr array is the same as the context you are saving
the "active" property (including spelling) is correctly configured in your model and the managed object (maybe you want to subclass for more clarity rather than relying on KVC).
there is something in the error variable
I also think there are some other design flaws. For example, you are getting cells and setting their selected state even though they might not even be visible. IMO, you should do this in cellForRowAtIndexPath, based on the state of the underlying managed object.
As for deselecting all other users in the same section you are right that a loop is probably inevitable. But I suppose it would be more efficient to fetch all users in a section at once and then loop through them to set the "active" property as desired.
I have a fetchedResultsController with a predicate, where "isOpen == YES"
When calling for closeCurrentClockSet, I set that property to NO. Therefore, it should no longer appear on my tableView.
For Some Reason, this is not happening.
Can someone help me figure this out please?
-(void)closeCurrentClockSet
{
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"isOpen == YES"];
NSArray *fetchedObjects =
[self fetchRequestForEntity:#"ClockSet"
withPredicate:predicate
inManagedObjectContext:[myAppDelegate managedObjectContext]];
ClockSet *currentClockSet = (ClockSet *)fetchedObjects.lastObject;
[currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];
}
--
I have a couple of methods more, using the exact same approach,
by calling a custom fetchRequestForEntity:withPredicate:inManagedObjectContext method.
In those methods, when changing a property, tableView get correctly updated!
But this one above (closeCurrentClockSet), doesn't! I can't figure out why.
--
My implementation for my fetchedResultsController, is from Apple's documentation.
Also, another detail. If I send my App, to the background. Close it and re-open, tableView shows updated as it should!
I have tried my best to follow previous questions here on stackOverflow. No luck. I also NSLogged this to the bone.
The object is getting correctly fetched. It is the right one. isOpen Property is being correctly updated to NO. But for some reason, my fetchedResultsController doesn't update tableView.
I did try a couple a "hammer" solutions, like reloadData and calling performFetch. But that didn't work. Or would make sense to used them...
EDIT: scratch that, it DID work, calling reloadData imediatly after performFetch on my resultsController but using reloadData is hammering a solution. Plus, it takes out all animations. I want my controller to auto-update my tableView.
Can someone help me figure this out?
Any help is greatly appreciated!
Thank you,
Nuno
EDIT:
The complete implementation.
fetchedResultsController is pretty standard and straightforward. Everything else is from Apple's documentation
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController) {
return _fetchedResultsController;
}
NSManagedObjectContext * managedObjectContext = [myAppDelegate managedObjectContext];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"ClockPair"
inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entity];
NSString *predicate = [NSString stringWithFormat: #"clockSet.isOpen == YES"];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:predicate]];
NSSortDescriptor *sortDescriptor1 =
[[NSSortDescriptor alloc] initWithKey:#"clockIn" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil
cacheName:#"Root"];
_fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
--
Boilerplate code from Apple's documentation:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationTop];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationLeft];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationTop];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id )sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
1ST UPDATE:
Tracking [managedObjectContext hasChanges] does return YES, as it should. But fetchedResultsController doesn't update the tableView
2ND UPDATE
didChangeObject:atIndexPath: does not get called for this particular case!
I have 2 more methods, with the EXACT same code, they just happen to be a different entity. And they work perfectly. Thank you #Leonardo for pointing this out
3TH UPDATE this method, follows the same rules. But does actually work.
- (void)clockOut
{
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"isOpen == %#", [NSNumber numberWithBool:YES]];
NSArray * fetchedObjects =
[self fetchRequestForEntity:#"ClockPair"
withPredicate:predicate
inManagedObjectContext:[myAppDelegate managedObjectContext]];
ClockPair *aClockPair = (ClockPair *)fetchedObjects.lastObject;
aClockPair.clockOut = [NSDate date];
aClockPair.isOpen = [NSNumber numberWithBool:NO];
}
Anyone has any other ideas for what I might be missing?
Thank you,
Nuno
OK, I will explain your problem, then I will let you judge whether it is a bug in FRC or not. If you think it is a bug, then you really should file a bug report with apple.
Your fetch result controller predicate is like this:
NSString *predicate = [NSString stringWithFormat: #"clockSet.isOpen == YES"];
which is a valid predicate for a boolean value. It is going to follow the relationship of the clockSet entity and grab its isOpen attribute. If it is YES then those objects will be accepted into the array of objects.
I think we are good up to here.
Now, if you change one of clockSet.isOpen attributes to NO, then you expect to see that object disappear from your table view (i.e., it should no longer match the predicate so it should be removed from the array of fetched objects).
So, if you have this...
[currentClockSet setIsOpen:[NSNumber numberWithBool:NO]];
then, whichever top-level object has a relationship to the currentClockSet should "disappear" from your FRC array of fetched results.
However, you do not see it disappear. The reason is that the object monitored by the FRC did not change. Yes, the predicate key path changed, but the FRC holds entities of ClockPair and a ClockSet entity actually changed.
You can watch the notifications fly around to see what's going on behind the scenes.
Anyway, the FRC will use a key path when you do a fetch, but it will not monitor changes to objects that are not in its actual set of fetched objects.
The easiest work-around is to "set" an attribute for the object that holds this key path object.
For example, I noticed that the ClockPair also has an isOpen attribute. If you have an inverse relationship, then you could do this...
currentClockSet.isOpen = NO;
currentClockSet.clockPair.isOpen = currentClockSet.clockPair.isOpen;
Notice that you did not actually change the value at all. However, the setter was called, which triggered KVO, and thus the private DidChange notification, which then told the FRC that the object changed. Thus, it re-evaluates the check to see if the object should be included, finds the keypath value changed, and does what you expect.
So, if you use a key path in your FRC predicate, if you change that value, you need to worm your way back to all the objects in the FRC array and "dirty them up" so that those objects are in the notification that is passed around about object changes. It's ugly, but probably better than saving or changing your fetch request and refetching.
I know you don't believe me, so go ahead and try it. Note, for it to work, you have to know which item(s) in the FRC array of objects would be affected by the change, and "poke" them to get the FRC to notice the change.
The other option, as I mentioned earlier, is to save the context, and refetch the values. If you don't want to save the context, you can make the fetch include updates in the current context, without refreshing from the store.
I have found that faking a change to an object that the FRC is watching is the best way to accomplish a re-evalution of predicates that are key paths to other entities.
OK, so, whether this is a bug or not is up for some debate. Personally, I think if the FRC is going to monitor a keypath, it should do it all the way, and not partially like we see here.
I hope that make sense, and I encourage you to file a bug report.
You ran into a similar problem.
I know this question is pretty old but I hope this helps someone else:
The easiest way was to introduce a new property named lastUpdated: NSDate in the parent object.
I had a Conversation which contains several Messages. Whenever the isRead flag of the message was updated, I needed an update in the ConversationOverviewViewController that only displays Conversations. Furthermore, the NSFetchedResultsController in the ConversationOverviewVC only fetches Conversations and doesn't know anything about a Message.
Whenever a message was updated, I called message.parentConversation.lastUpdated = NSDate(). It's an easy and useful way to trigger the update manually.
Hope this helps.
After [currentClockSet setIsOpen:[NSNumber numberWithBool:NO]]; can you save the managed object context:
NSError *saveError = nil;
if( ![[myAppDelegate managedObjectContext] save:&saveError] ) {
// handle error saving context
}
I suspect your UITableView will update properly after saving the context. This is likely why sending your app to the background works. I suspect that your Core Data stack is set up in the application's delegate in such a way that it performs a save on the main NSManagedObjectContext when it goes into the background.
I have a parent-child Core Data relationship set up in my iPhone app. I have a Manufacturer object and a Car object. It is a to-many relationship with the Manufacturer being the owner. The main view is a Table View containing the Manufacturers. The detail view is another Table View with the different types of cars. I have been using Tim Roadley's Core Data Tutorial as the base. This tutorial uses Stanford's Core Data Table View Library for the table views.
Adding Cars and Manufacturers gives me no problem, but when I go in and delete with multiple cars in the table view I get this error:
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1914.84/UITableView.m:1037
2012-07-29 23:39:33.561 App [16368: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 (0) 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 (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).'
If I delete the only car, it works fine until I try to add a new car, when I get this error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'manufacturer' between objects in different contexts (source = <Car: 0x6d96da0> (entity: Car; id: 0x6d8a3c0 <x-coredata:///Car/tC78E17EB-1D68-4998-8C4D-6D1199CE253F4> ; data: {
dateAdded = nil;
manufacturer = nil;
carName = new;
}) , destination = <Manufacturer: 0x6bb1f40> (entity: Manufacturer; id: 0x6d87340 <x-coredata://2E8DDF34-B01A-4203-A53E-73DBE6A2F976/Garden/p6> ; data: <fault>))'
Here is my editing method:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.tableView beginUpdates];
Plant *plantToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Deleting plant '%#'", plantToDelete.plantName);
[self.managedObjectContext deleteObject:plantToDelete];
[self.managedObjectContext save:nil];
//delete empty tableview row
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
NSLog(#"Before performFetch...");
[self performFetch];
NSLog(#"After performFetch...");
[self.tableView endUpdates];
}
}
The performFetch method is contained in the previously mentioned CoreDataTableViewController files. For your convenience, here it is:
(void)performFetch
{
_debug = YES;
if (self.fetchedResultsController) {
if (self.fetchedResultsController.fetchRequest.predicate) {
if (self.debug) NSLog(#"[%# %#] fetching %# with predicate: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName, self.fetchedResultsController.fetchRequest.predicate);
} else {
if (self.debug) NSLog(#"[%# %#] fetching all %# (i.e., no predicate)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName);
}
NSError *error;
[self.fetchedResultsController performFetch:&error];
if (error) NSLog(#"[%# %#] %# (%#)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [error localizedDescription], [error localizedFailureReason]);
} else {
if (self.debug) NSLog(#"[%# %#] no NSFetchedResultsController (yet?)", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
}
[self.tableView reloadData];
}
According to other questions, I am doing this correctly by using beginUpdates and endUpdates. This is a puzzling error. Thanks for your help.
I am not sure why you are performing the fetch again, if an object is removed from the context, the fetched results controller is aware of that change already. I think the main problem you have is calling perform fetch in the middle of processing updates to the table. If you comment that line out, does it still have the error?
Additionally, the following may or may not be another part of the the problem as this is where you are differing from my own code:
I have not seen the begin/end edits calls in tableView:CommitEditingStyle: before. My own process in that method generally deletes the object without any concern for the table row. The table rows are reconciled in the fetchedResultController delegate methods like so:
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
//the fetch controller is about to start sending change notifications so prepare the tableview
[self.tableView beginUpdates];
}
-(void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
// reconcile your rows here
switch(type) {
case NSFetchedResultsChangeInsert:
break;
case NSFetchedResultsChangeDelete:
// this one is you
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeUpdate:
break;
case NSFetchedResultsChangeMove:
break;
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
As long as the number of rows matches the number of fetched objects after all that, you should not have that error.
Try removing the lines
//delete empty tableview row
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
NSLog(#"Before performFetch...");
[self performFetch];
NSLog(#"After performFetch...");
I believe that CoreDataTableViewController will automatically handle removing the row from the table. You are essentially removing the row twice which is causing the error.
I was having the same error occur when I would open a tableViewController that was a subclass of Tim Roadley's CoreDataTableViewController. My specific app does not require the user to be able to add or delete rows, but it allows them to reorder the fetched results by name and by distance as well as search the data. I used Dean David's answer (the accepted answer above), but after every case statement I only added a break statement. So far that has worked for this app!
I am trying to asynchronously download images for a UITableViewCell, but it is currently setting the same image to each cell.
Please can you tell me the problem with my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
SearchObject *so = (SearchObject *)[_tableData objectAtIndex:indexPath.row];
cell.textLabel.text = [[[[so tweet] stringByReplacingOccurrencesOfString:#""" withString:#"\""] stringByReplacingOccurrencesOfString:#"<" withString:#"<"] stringByReplacingOccurrencesOfString:#">" withString:#">"];
cell.detailTextLabel.text = [so fromUser];
if (cell.imageView.image == nil) {
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:[so userProfileImageURL]]];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
[conn start];
}
if ([_cellImages count] > indexPath.row) {
cell.imageView.image = [UIImage imageWithData:[_cellImages objectAtIndex:indexPath.row]];
}
return cell;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_cellData appendData:data];
[_cellImages addObject:_cellData];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.tableView reloadData];
}
You are appending the data from every image downloaded to the same data object. So in the best case the data object ends up with the data for image #1 immediately followed by the data for image #2 and so on; the image decoder is apparently taking the first image in the chunk of data and ignoring the garbage after. You also seem to be unaware that NSURLConnections' connection:didReceiveData: will not necessarily be called in the order that the connections were started, that connection:didReceiveData: can be called zero or multiple times per connection (and probably will if your images are more than a few kibibytes), and that tableView:cellForRowAtIndexPath: is not guaranteed to be called for every cell in the table in order. All of which are going to totally screw up your _cellImages array.
To do this right, you need to have a separate NSMutableData instance for each connection, and you need to add it to your _cellImages array just once, and at the correct index for the row rather than at the arbitrary next available index. And then in connection:didReceiveData: you need to figure out the correct NSMutableData instance to append to; this could be done by using the connection object (wrapped in an NSValue using valueWithNonretainedObject:) as the key in an NSMutableDictionary, or using objc_setAssociatedObject to attach the data object to the connection object, or by making yourself a class that handles all the management of the NSURLConnection for you and hands you the data object when complete.
I don't know if this is causing the problem or not, but in your connection:didReceiveData: method you're just appending the image data to the array; you should be storing the image data in such a way that you can link it to the cell it's supposed to be shown in. One way to do this would be use an NSMutableArray populated with a bunch of [NSNull]s, then replace the null value at the appropriate index when the connection has finished loading.
Also, you're appending the _cellData to the _cellImages array when the connection hasn't finished loading, you should only be doing this in the connection:didFinishLoading method.