I am using Justin Driscoll's article on Core Data with UIManagedDocument in singleton pattern to set it up for UITabViewController. I am running the app on Simulator. Its working fine for the first time. The database is created successfully and I can see data in the tableview controller for each tab. But when I restart my application, the tableviews are empty because NSFetchRequest fetches 0 matches for the entity. The same fetch request fetches correct result during the first run.
I think its something to do with asynchronous nature of loading data and data not autosaving before I stop the app in simulator. So data is not available in second run of app.
The way I am doing my data loading as seen in the code. The fetchDataIntoDocument method does the initial loading of data.
// Document Handler Singleton Class
-(void) performWithDocument:(OnDocumentReady)onDocumentReady {
void (^OnDocumentDidLoad)(BOOL) = ^(BOOL Success) {
onDocumentReady(self.document);
};
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
**[self fetchDataIntoDocument:self.document];**
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateClosed) {
[self.document openWithCompletionHandler:OnDocumentDidLoad];
} else if (self.document.documentState == UIDocumentStateNormal) {
OnDocumentDidLoad(YES);
}
}
-(void)fetchDataIntoDocument:(UIManagedDocument *)document {
MyEntityDataController *dc= [[MyEntityDataController alloc] init];
NSDictionary *entityInfo =[dc getEntityInfo];
[document.managedObjectContext performBlock:^{
[Entity createEntityWithInfo:entityInfo inManagedObjectContext:document.managedObjectContext];
}];
}
My TableViewController class
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (!self.databaseDocument) {
[[LTDatabaseDocumentHandler sharedDatabaseDocumentHandler] performWithDocument:^ (UIManagedDocument *document) {
self.databaseDocument = document;
[self populateTableViewArrayFromDocument:self.databaseDocument];
}];
}
}
Within populateTableViewArrayFromDocument I am executing my fetch request
-(void)populateTableViewArrayFromDocument:(UIManagedDocument *)document
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Entity2"];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
NSError *error = nil;
NSArray *matches = [self.databaseDocument.managedObjectContext executeFetchRequest:request error:&error];
NSLog(#" matches count for Entity2 %d", matches.count);
for (Entity2 *entity2 in matches) {
//do stuff with data and add it to tableview array
}
}
I think I have found why you have this problem. I have just run into this issue and it took some research to figure it out. Basically, you are right. The problem is indeed in the asynchronous nature of UIManagedDocument. You need to wait until the document loads into memory and then do your fetching.
This is the code I use to make sure the document is ready:
if ([[NSFileManager defaultManager] fileExistsAtPath:[_URLDocument path]]) {
[_managedDocument openWithCompletionHandler:^(BOOL success){
[self ready]
if (!success) {
// Handle the error.
}
}];
}
Hope this helps, cheers!
Related
Im trying to retrieve an array of sorted objects from my Core Data backed NSArrayController. Once the ManagedObjectContext is ready I fire a Notification which the object owning the NSArrayController listens for and then sets up the fetch request with sortDescriptors. However the data is NEVER sorted. I've been pounding my head on this for several hours now.
I get the expected data in the array but it's not sorted at all. Anyone have any ideas where I am going wrong?
// Elsewhere in code
...
_charts = [[NSArrayController alloc] init];
_charts.entityName = #"Charts";
...
// When Cored Data and Managed Object Context is ready
-(void)mangagedObjectContextReady
{
NSManagedObjectContext *moc = ((AppDelegate *)[NSApp delegate]).managedObjectContext;
_charts.managedObjectContext = moc;
_charts.usesLazyFetching = NO;
_charts.automaticallyPreparesContent = YES;
_charts.automaticallyRearrangesObjects = YES;
// Create fetch request for data
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.entity = [NSEntityDescription entityForName:#"Charts" inManagedObjectContext:moc];
// Create the sort descriptors for the Charts entity
// Name and chartType are properties on the entity "Charts"
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES selector:#selector(caseInsensitiveCompare:)];
NSSortDescriptor *chartTypeDescriptor = [[NSSortDescriptor alloc] initWithKey:#"chartType" ascending:YES selector:#selector(caseInsensitiveCompare:)];
fetchRequest.sortDescriptors = #[nameDescriptor, chartTypeDescriptor];
NSError *error = nil;
BOOL success = [_charts fetchWithRequest:fetchRequest merge:NO error:&error];
if(!success)
NSLog(#"Error %#:", [error localizedDescription]);
else {
[_charts rearrangeObjects];
[_tv reloadData];
}
I use this method to get an array or sorted objects.. but data is not sorted as per above sort descriptors?
[_charts.arrangedObjects objectAtIndex:row]
_charts.sortDescriptors = #[nameDescriptor, chartTypeDescriptor];
I know there are many threads about NSManagedObjectContexts and threads but my problem seems to be only specific to iOS7. (Or at least not visible in OS6)
I have an app that makes use of dispatch_queue_ and runs multiple threads to fetch data from the server and update the UI. The app was working fine on iOS6 but on iOS7 it seems to get into deadlocks(mutex wait). See below the stack trace -
The "wait" happens in different methods usually when executing a fetch request and saving a (different) context. The commit Method is as follows :
-(void)commit:(BOOL) shouldUndoIfError forMoc:(NSManagedObjectContext*)moc {
#try {
// shouldUndoIfError = NO;
// get the moc for this thread
NSManagedObjectContext *moc = [self safeManagedObjectContext];
NSThread *thread = [NSThread currentThread];
NSLog(#"got login");
if ([thread isMainThread] == NO) {
// only observe notifications other than the main thread
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:moc];
NSLog(#"not main thread");
}
NSError *error;
if (![moc save:&error]) {
// fail
NSLog(#"ERROR: SAVE OPERATION FAILED %#", error);
if(shouldUndoIfError) {
[moc undo];
}
}
if ([thread isMainThread] == NO) {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSManagedObjectContextDidSaveNotification
object:moc];
}
}
#catch (NSException *exception) {
NSLog(#"Store commit - %#",exception);
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:#"name",#"store commit",#"exception", exception.description, nil];
[Flurry logEvent:#"MyException" withParameters:dictionary timed:YES];
}
#finally {
NSLog(#"Store saved");
}
}
How I'm creating new contexts for each thread :
-(NSManagedObjectContext *)safeManagedObjectContext {
#try {
if(self.managedObjectContexts == nil){
NSMutableDictionary *_dict = [[NSMutableDictionary alloc]init];
self.managedObjectContexts = _dict;
[_dict release];
_dict = nil;
}
NSManagedObjectContext *moc = self.managedObjectContext;
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread]) {
return moc;
}
// a key to cache the context for the given thread
NSString *threadKey = [NSString stringWithFormat:#"%p", thread];
if ( [self.managedObjectContexts valueForKey:threadKey] == nil) {
// create a context for this thread
NSManagedObjectContext *threadContext = [[[NSManagedObjectContext alloc] init] retain];
[threadContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[threadContext setPersistentStoreCoordinator:[moc persistentStoreCoordinator]];
[threadContext setUndoManager:nil];
// cache the context for this thread
[self.managedObjectContexts setObject:threadContext forKey:threadKey];
NSLog(#"added a context to dictionary, length is %d",[self.managedObjectContexts count]);
}
return [self.managedObjectContexts objectForKey:threadKey];
}
#catch (NSException *exception) {
//
}
#finally {
//
}
}
What I have so far :
One Persistent Store coordinator.
Each New thread has its own Managed Object Context.
Strange part is that the same code worked fine on OS6 but not on OS7. I am still using the xcode4.6.3 to compile the code. Most of the code works on this principle, I run a thread, fetch data, commit it and then post a notification. Could the freeze/deadlock be because the notification gets posted and my UI elements fetch the data before the save(&merge) are reflected? Anything else that I'm missing ?
I'm working with TWrequest to display my twitter lists in a tableview. The following code works. The problem is it is very slow to update the table. I am NSlogging the request response (which happens very quickly), I am also looping through each list and adding the list 'name' to an array (which again, happens very quickly <1s). But for some inexplicable reason, the table takes roughly a further 4 seconds or so to update.
Why is this taking so long for the table to reload? The problem is not parsing the response (because I can see with nslog this happens pretty quick), it's taking a long time to display in the table? Help very much appreciated!
-(IBAction)getLists{
// First, we need to obtain the account instance for the user's Twitter account
ACAccountStore *store = [[ACAccountStore alloc] init];
ACAccountType *twitterAccountType = [store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
// Request permission from the user to access the available Twitter accounts
[store requestAccessToAccountsWithType:twitterAccountType withCompletionHandler:^(BOOL granted, NSError *error) {
if (!granted) {
// The user rejected your request
NSLog(#"User rejected access to the account.");
}
else {
// Grab the available accounts
twitterAccounts = [store accountsWithAccountType:twitterAccountType];
if ([twitterAccounts count] > 0) {
// Use the first account for simplicity
ACAccount *account = [twitterAccounts objectAtIndex:0];
// Now make an authenticated request to our endpoint
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
//[params setObject:#"1" forKey:#"include_entities"];
// The endpoint that we wish to call
NSURL *url = [NSURL URLWithString:#"http://api.twitter.com/1.1/lists/list.json"];
// Build the request with our parameter
TWRequest *request = [[TWRequest alloc] initWithURL:url parameters:params requestMethod:TWRequestMethodGET];
// Attach the account object to this request
[request setAccount:account];
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (!responseData) {
// inspect the contents of error
NSLog(#"error = %#", error);
}
else {
NSError *jsonError;
NSArray *timeline = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingMutableLeaves error:&jsonError];
if (timeline) {
// at this point, we have an object that we can parse
NSLog(#"timeline = %#", timeline);
for (NSDictionary *element in timeline) {
NSString *listName = [element valueForKey:#"name"];
[listsArray addObject:listName];
}
[listsTable reloadData];
}
else {
// inspect the contents of jsonError
NSLog(#"jsonerror = %#", jsonError);
}
}
}];
}
}
}];
}
Sorry, just came across this post. If you haven't found a solution yet, hopefully this will help.
I believe that performRequestWithHandler can be called on any thread, so UI changes should be dispatched to the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
//update UI here
});
Or in the case of reloading table data you can use:
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
I have a view controller that is a subclass of UITableViewController. Here is my viewWillAppear:animated method:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (fetchedResultsController != nil) {
[fetchedResultsController release];
fetchedResultsController = nil;
}
[self.fetchedResultsController performFetch:nil];
[self.tableView reloadData];
}
I am getting confused by seeing the fetchedResultsController being accessed when [super viewWillAppear:animated] is called. Since super is a UITableViewController, and there is no viewWillAppear:animated method for that class, per se, then its superclass viewWillAppear:animated should be called, right? If that's correct, then the UIViewController class should not be accessing UITableViewController delegate methods. But I see that numberOfSectionsInTableView is getting called. I'm not sure why the call to super viewWillAppear:animated would do this.
So before I explicitly run the peformFetch and reloadData, the table is getting populated. At that time, the data it is being populated with is out of date.
Here is the fetchedResultsController code
- (NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSFetchRequest *fetchRequest = ...
NSEntityDescription * entity = ...
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:10];
NSSortDescriptor *aSortDescriptor = ...
NSSortDescriptor *bSortDescriptor = ...
NSArray *sortDescriptors = ...
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = ...
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
...
[sortDescriptors release];
NSError *error = nil;
if (![fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved Error %#, %#", error, [error userInfo]);
abort();
}
return fetchedResultsController;
}
The documentation specifically describes this behaviour:
When the table view is about to appear the first time it’s loaded, the table-view controller reloads the table view’s data. It also clears its selection (with or without animation, depending on the request) every time the table view is displayed. The UITableViewController class implements this in the superclass method viewWillAppear:. You can disable this behavior by changing the value in the clearsSelectionOnViewWillAppear property.
I'm relatively new to Core Data on iOS, but I think I've been getting better with it. I've been experiencing a bizarre crash, however, in one of my applications and have not been able to figure it out.
I have approximately 40 objects in Core Data, presented in a UITableView. When tapping on a cell, a UIActionSheet appears, presenting the user with a UIActionSheet with options related to the cell that was selected. So that I can reference the selected object, I declare an NSIndexPath in my header called "lastSelection" and do the following when the UIActionSheet is presented:
// Each cell has a tag based on its row number (i.e. first row has tag 0)
lastSelection = [NSIndexPath indexPathForRow:[sender tag] inSection:0];
NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:lastSelection];
BOOL onDuty = [[managedObject valueForKey:#"onDuty"] boolValue];
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:#"Status" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
if(onDuty) {
[actionSheet addButtonWithTitle:#"Off Duty"];
} else {
[actionSheet addButtonWithTitle:#"On Duty"];
}
actionSheet.actionSheetStyle = UIActionSheetStyleBlackOpaque;
// Override the typical UIActionSheet behavior by presenting it overlapping the sender's frame. This makes it more clear which cell is selected.
CGRect senderFrame = [sender frame];
CGPoint point = CGPointMake(senderFrame.origin.x + (senderFrame.size.width / 2), senderFrame.origin.y + (senderFrame.size.height / 2));
CGRect popoverRect = CGRectMake(point.x, point.y, 1, 1);
[actionSheet showFromRect:popoverRect inView:[sender superview] animated:NO];
[actionSheet release];
When the UIActionSheet is dismissed with a button, the following code is called:
- (void)actionSheet:(UIActionSheet *)actionSheet willDismissWithButtonIndex:(NSInteger)buttonIndex {
// Set status based on UIActionSheet button pressed
if(buttonIndex == -1) {
return;
}
NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:lastSelection];
if([actionSheet.title isEqualToString:#"Status"]) {
if([[actionSheet buttonTitleAtIndex:buttonIndex] isEqualToString:#"On Duty"]) {
[managedObject setValue:[NSNumber numberWithBool:YES] forKey:#"onDuty"];
[managedObject setValue:#"onDuty" forKey:#"status"];
} else {
[managedObject setValue:[NSNumber numberWithBool:NO] forKey:#"onDuty"];
[managedObject setValue:#"offDuty" forKey:#"status"];
}
}
NSError *error;
[self.managedObjectContext save:&error];
[tableView reloadData];
}
This might not be the most efficient code (sorry, I'm new!), but it does work. That is, for the first 25 items in the list. Selecting the 26th item or beyond, the UIActionSheet will appear, but if it is dismissed with a button, I get a variety of errors, including any one of the following:
[__NSCFArray section]: unrecognized selector sent to instance 0x4c6bf90
Program received signal: “EXC_BAD_ACCESS”
[_NSObjectID_48_0 section]: unrecognized selector sent to instance 0x4c54710
[__NSArrayM section]: unrecognized selector sent to instance 0x4c619a0
[NSComparisonPredicate section]: unrecognized selector sent to instance 0x6088790
[NSKeyPathExpression section]: unrecognized selector sent to instance 0x4c18950
If I comment out NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:lastSelection]; it doesn't crash anymore, so I believe it has something do do with that. Can anyone offer any insight? Please let me know if I need to include any other information. Thanks!
EDIT: Interestingly, my fetchedResultsController code returns a different object every time. Is this expected, or could this be a cause of my issue? The code looks like this:
- (NSFetchedResultsController *)fetchedResultsController {
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Employee" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:80];
// Edit the sort key as appropriate.
NSString *sortKey;
BOOL ascending;
if(sortControl.selectedSegmentIndex == 0) {
sortKey = #"startTime";
ascending = YES;
} else if(sortControl.selectedSegmentIndex == 1) {
sortKey = #"name";
ascending = YES;
} else {
sortKey = #"onDuty";
ascending = NO;
}
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortKey ascending:ascending];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
NSError *error = nil;
if (![fetchedResultsController_ performFetch:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
//NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return fetchedResultsController_;
}
This happens when I set a breakpoint:
(gdb) po [self fetchedResultsController]
<NSFetchedResultsController: 0x61567c0>
(gdb) po [self fetchedResultsController]
<NSFetchedResultsController: 0x4c83630>
It's prob the case that self.fetchedResultsController is pointing to the wrong memory location. You will need to check if the object has been retained.
Figured it out! Looks like it was an issue with autoreleased objects.
When I turned on NSZombieEnabled, I got this:
*** -[NSIndexPath section]: message sent to deallocated instance 0xa674530
I simply changed lastSelection = [NSIndexPath indexPathForRow:[sender tag] inSection:0]; to lastSelection = [[NSIndexPath indexPathForRow:[sender tag] inSection:0] retain]; and that took care of it.