I'm asynchronously fetching data, and I've used this as a guide: http://deeperdesign.wordpress.com/2011/05/30/cancellable-asynchronous-searching-with-uisearchdisplaycontroller/
In
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString{
//setup request / predicate etc...
[self.searchQueue addOperationWithBlock:^{
NSError *error;
self.matchingObjects = [self.managedObjectContext executeFetchRequest:request error:&error];
[request release];
[[NSOperationQueue mainQueue] addOperationWithBlock:^
{
[self.searchDisplayController.searchResultsTableView reloadData];
}];
}];
// Return YES to cause the search result table view to be reloaded.
return NO;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
// Return the number of rows in the section.
return [self.matchingObjects count];
}
Every now and then I'll get something to the effect of:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSArray objectAtIndex:]: index 0 beyond bounds for empty array'
This is thrown at the matchingObjects ivar when accessing it to construct the table cell in:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
The crash doesn't occur all the time, just seems to happen on random occasions. I'm guessing that somewhere the count on the matchingObjects array is returning a certain value, which changes and is not being updated.
I'm not entirely sure of how to deal with this - been looking over this for hours, is there something I'm missing?
I figured out what it was - took me a while, but I looked again at the example that I just linked. I was updating the self.matchingObjects iVar in the background thread, which on some occasions caused a mismatch between the range of the array available in the main thread and the background thread. So for example, the variable may have been updated in the background thread, and the main thread may still be accessing a part of the range that no longer exists in the variable since it was updated.
Fixed it by amending my code as follows:
[self.searchQueue addOperationWithBlock:^
{
NSError *error;
NSArray *results = [self.managedObjectContext executeFetchRequest:request error:&error];
[request release];
[[NSOperationQueue mainQueue] addOperationWithBlock:^
{
self.matchingObjects = results;
[self.searchDisplayController.searchResultsTableView reloadData];
}];
}];
Now the results of the search are loaded into a temporary holding array named "results", and the matchingObjects iVar is first updated in the main thread and then the tableView is reloaded. This way, the tableView always is referring to an array that is never changed whilst it is being accessed, since the tableView relies on matchingObjects to get the number of rows and data.
Related
I'm experimenting with Firebase's FDataSnapshot to pull in data and I would like it to write its data to my core data using MagicalRecord.
According to Firebases "best practice" blog I need to keep a reference to the "handle" so it can be cleaned up later on. Further, they mention to put the FDSnapshot code in viewWillAppear.
I am wanting a callback so that when its finished doing its thing to update core data.
But I'm really note sure how to do that; its doing two things and giving a return at the same time.
// In viewWillAppear:
__block NSManagedObjectContext *context = [NSManagedObjectContext MR_context];
self.handle = [self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
if (snapshot.value == [NSNull null])
{
NSLog(#"Cannot find any data");
}
else
{
NSArray *snapshotArray = [snapshot value];
// cleanup to prevent duplicates
[FCFighter MR_truncateAllInContext:context];
for (NSDictionary *dict in snapshotArray)
{
FCFighter *fighter = [FCFighter insertInManagedObjectContext:context];
fighter.name = dict[#"name"];
[context MR_saveToPersistentStoreWithCompletion:^(BOOL contextDidSave, NSError *error){
if (error)
{
NSLog(#"Error - %#", error.localizedDescription);
}
}];
}
}
}];
NSFetchRequest *fr = [[NSFetchRequest alloc] initWithEntityName:[FCFighter entityName]];
fr.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.fighterList = (NSArray *) [context executeFetchRequest:fr error:nil];
[self.tableView reloadData];
In the above code, the core data reading does not wait for the firebase to complete.
Thus, my query -- how would I best combine a completion handler so that when it is complete to update core data, and reload the tableview.
Many thanks
This is a common issue when working with Asynchronous data.
The bottom line is that all processing of data returned from an async call (in this case, the snapshot) needs to be done inside the block.
Anything done outside the block may happen before the data is returned.
So some sudo code
observeEvent withBlock { snapshot
//it is here where snapshot is valid. Process it.
NSLog(#"%#", snapshot.value)
}
Oh, and a side note. You really only need to track the handle reference when you are going to do something else with it later. Other than that, you can ignore the handles.
So this is perfectly valid:
[self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
//load your array of tableView data from snapshot
// and/or store it in CoreData
//reload your tableview
}
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 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'm developing an iOS app with a view containing a TableView.
Some method receives data from the web, opens a new thread to calculate information and inserts a row into the table at run time with the method: insertRowsAtIndexPaths.
Now if a lot of data is coming at once, the table may update itself after a few insertions and not after each one, and thats provokes an exception saying that the number of rows in section isn't right (that's because it thinks it should have an increment of one row but the threads already inserted the array of data some more cells).
Even if I make a lock on the insertion to the datasource array and the insertRowsAtIndexPaths method, it's still do the same.
NSLock *mylock = [[NSLock alloc] init];
[mylock lock];
[array addObject:object];
[tableView insertRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationLeft];
[mylock unlock];
help please,
Thank you!
you have to run this method on the main thread. All User Interface interaction has to be done on the main thread.
Let's say your method looks like this:
- (void)addSomeObject:(id)object {
[array addObject:object];
[tableView insertRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationLeft];
}
and you are calling it like this:
[self addSomeObject:anObject];
then you would change this call to something like this:
[self performSelectorOnMainThread:#selector(addSomeObject:) withObject:anObject waitUntilDone:NO];
I have hit the proverbial wall trying to figure out how to populate an NSImage with data returned from an asynchronous NSURLConnection in my desktop app (NOT an iPhone application!!).
Here is the situation.
I have a table that is using custom cells. In each custom cell is an NSImage which is being pulled from a web server. In order to populate the image I can do a synchronous request easily:
myThumbnail = [[NSImage alloc] initWithContentsOfFile:myFilePath];
The problem with this is that the table blocks until the images are populated (obviously because it's a synchronous request). On a big table this makes scrolling unbearable, but even just populating the images on the first run can be tedious if they are of any significant size.
So I create an asynchronous request class that will retrieve the data in its own thread as per Apple's documentation. No problem there. I can see the data being pulled and populated (via my log files).
The problem I have is once I have the data, I need a callback into my calling class (the custom table view).
I was under the impression that I could do something like this, but it doesn't work because (I'm assuming) that what my calling class really needs is a delegate:
NSImage * myIMage;
myImage = [myConnectionClass getMyImageMethod];
In my connection class delegate I can see I get the data, I just don't see how to pass it back to the calling class. My connectionDidFinishLoading method is straight from the Apple docs:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// do something with the data
// receivedData is declared as a method instance elsewhere
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
// release the connection, and the data object
[connection release];
[receivedData release];
}
I am hoping this is a simple problem to solve, but I fear I am at the limit of my knowledge on this one and despite some serious Google searches and trying many different recommended approaches I am struggling to come up with a solution.
Eventually I will have a sophisticated caching mechanism for my app in which the table view checks the local machine for the images before going out and getting them form the server and maybe has a progress indicator until the images are retrieved. Right now even local image population can be sluggish if the image's are large enough using a synchronous process.
Any and all help would be very much appreciated.
Solution Update
In case anyone else needs a similar solution thanks to Ben's help here is what I came up with (generically modified for posting of course). Bear in mind that I have also implemented a custom caching of images and have made my image loading class generic enough to be used by various places in my app for calling images.
In my calling method, which in my case was a custom cell within a table...
ImageLoaderClass * myLoader = [[[ImageLoaderClass alloc] init] autorelease];
[myLoader fetchImageWithURL:#"/my/thumbnail/path/with/filename.png"
forMethod:#"myUniqueRef"
withId:1234
saveToCache:YES
cachePath:#"/path/to/my/custom/cache"];
This creates an instance of myLoader class and passes it 4 parameters. The URL of the image I want to get, a unique reference that I use to determine which class made the call when setting up the notification observers, the ID of the image, whether I want to save the image to cache or not and the path to the cache.
My ImageLoaderClass defines the method called above where I set what is passed from the calling cell:
-(void)fetchImageWithURL:(NSString *)imageURL
forMethod:(NSString *)methodPassed
withId:(int)imageIdPassed
saveToCache:(BOOL)shouldISaveThis
cachePath:(NSString *)cachePathToUse
{
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create the connection with the request and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
// Create the NSMutableData that will hold
// the received data
// receivedData is declared as a method instance elsewhere
receivedData = [[NSMutableData data] retain];
// Now set the variables from the calling class
[self setCallingMethod:methodPassed];
[self setImageId:imageIdPassed];
[self setSaveImage:shouldISaveThis];
[self setImageCachePath:cachePathToUse];
} else {
// Do something to tell the user the image could not be downloaded
}
}
In the connectionDidFinishLoading method I saved the file to cache if needed and made a notification call to any listening observers:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
// Create an image representation to use if not saving to cache
// And create a dictionary to send with the notification
NSImage * mImage = [[NSImage alloc ] initWithData:receivedData];
NSMutableDictionary * mDict = [[NSMutableDictionary alloc] init];
// Add the ID into the dictionary so we can reference it if needed
[mDict setObject:[NSNumber numberWithInteger:imageId] forKey:#"imageId"];
if (saveImage)
{
// We just need to add the image to the dictionary and return it
// because we aren't saving it to the custom cache
// Put the mutable data into NSData so we can write it out
NSData * dataToSave = [[NSData alloc] initWithData:receivedData];
if (![dataToSave writeToFile:imageCachePath atomically:NO])
NSLog(#"An error occured writing out the file");
}
else
{
// Save the image to the custom cache
[mDict setObject:mImage forKey:#"image"];
}
// Now send the notification with the dictionary
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:callingMethod object:self userInfo:mDict];
// And do some memory management cleanup
[mImage release];
[mDict release];
[connection release];
[receivedData release];
}
Finally in the table controller set up an observer to listen for the notification and send it off to the method to handle re-displaying the custom cell:
-(id)init
{
[super init];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(updateCellData:) name:#"myUniqueRef" object:nil];
return self;
}
Problem solved!
My solution is to use Grand Central Dispatch (GCD) for this purpose, you could save the image to disc too in the line after you got it from the server.
- (NSView *)tableView:(NSTableView *)_tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
SomeItem *item = [self.items objectAtIndex:row];
NSTableCellView *cell = [_tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
if (item.artworkUrl)
{
cell.imageView.image = nil;
dispatch_async(dispatch_queue_create("getAsynchronIconsGDQueue", NULL),
^{
NSURL *url = [NSURL URLWithString:item.artworkUrl];
NSImage *image = [[NSImage alloc] initWithContentsOfURL:url];
cell.imageView.image = image;
});
}
else
{
cell.imageView.image = nil;
}
return cell;
}
(I am using Automatic Reference Counting (ARC) therefore there are no retain and release.)
Your intuition is correct; you want to have a callback from the object which is the NSURLConnection’s delegate to the controller which manages the table view, which would update your data source and then call -setNeedsDisplayInRect: with the rect of the row to which the image corresponds.
Have you tried using the initWithContentsOfURL: method?