I have an IOS app that uses a core database to store
thumbnail photo images and their user selected rating.
I use multiple managed object contexts for this.
The problem is that when the app is installed and
launched for the first time (and thus creates a new
database) the main MOC does not see updates from the
temporary MOC that is making changes to the photo rating.
However on subsequent launches of the app (i.e. database
exists already), everything works fine each and every
time.
And additionally, on a new app launch, even though the
ratings don't show up in the main viewcontroller, I know
they're being saved to disk, because on a app re-launch
I see the ratings the user had entered.
The main MOC is a list view controller that displays the
photos. When the user selects a photo from the list, it
launches another view controller (with a temporary MOC
tied to the same persistent store) where the user selects
a photo rating. But on a fresh launch of the app, the photo
rating setting never propagates back to the list view controller.
I've included some code. Would appreciate any insights.
Database creation in main listview controller
if ([fileManager fileExistsAtPath:[urlForPhotosDb path]]) {
if (photosDB.documentState == UIDocumentStateClosed) {
[photosDB openWithCompletionHandler:^(BOOL success) {
......(additional code here).........
}];
}
} else {
[photosDB saveToURL:urlForPhotosDb forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
[PhotosDatabase populatePhotosDb];
......(additional code here).........
];
}
return photosDB;
}
On View Load in main listView controller
(void)viewDidLoad
{
[super viewDidLoad];
[PhotosDatabase getPhotosDbForOpenBlock:^(UIManagedDocument *doc) {
self.psc = [doc.managedObjectContext persistentStoreCoordinator];
[self setupFetchedResultsController:doc];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(contextSaved:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
}];
}
Setting up of temporary MOC in delegate method of main
listview controller called from secondary view controller
-(void)didSelectPhotoRating:(NSDictionary *)photoInfo Rating:(NSNumber *)rating
{
NSManagedObjectContext *newContext = [[NSManagedObjectContext alloc] init];
[newContext setPersistentStoreCoordinator:self.psc];
Photos *photo = [Photos findPhoto:photoInfo
inManagedObjectContext:newContext];
if (photo) {
photo.rating = rating;
NSError *error;
[newContext save:&error];
}
}
method in main listview controller to merge changes
-(void)contextSaved:(NSNotification *)notification
{
if ([notification object] != self.document.managedObjectContext) {
[self.document.managedObjectContext
mergeChangesFromContextDidSaveNotification:notification];
[self performFetch];
[self.tableView reloadData];
}
}
And in the contextSaved method above, the notification
indeed contains the user selected rating for the photo.
But it doesn't reflect in the main listview controller.
Start using nested contexts. It's just much much much much much easier and in sync on every possible level. The have not updated the core data programming guide to cover it well at this point.
http://www.cocoanetics.com/2012/07/multi-context-coredata/
Related
Background info
I've almost completed my app. Everything was working perfectly. Then the client asked for logging in the app (i.e. various points that had to record what was done, what responses were, etc...).
The app allows the user to create "messages" which are saved into core-data. The messages are then uploaded to the server individually. The message are created on the main thread and uploaded in an NSOperation subclass on a background thread.
It is the same template for the NSOperation subclass that I have used before and works. I'm doing all the best practise stuff for multi-threaded core-data.
All this side of the app works fine.
I added the logging part of the app. I've created a singleton called MyLogManager and a CoreData entity called LogEntry. The entity is very simple, it only has a date and text.
Code
The function inside the MyLogManager is...
- (void)newLogWithText:(NSString*)text
{
NSLog(#"Logging: %#", text);
NSManagedObjectContext *context = [self context];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"LogEntry" inManagedObjectContext:context];
LogEntry *logEntry = [[LogEntry alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
logEntry.text = text;
logEntry.date = [NSDate date];
[self saveContext:context];
}
which in turn runs...
- (NSManagedObjectContext*)context
{
AppDelegate *appDelegate = (ThapAppDelegate*)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[managedObjectContext setPersistentStoreCoordinator:appDelegate.persistentStoreCoordinator];
return managedObjectContext;
}
and
- (void)saveContext:(NSManagedObjectContext*)context
{
MyAppDelegate *appDelegate = (MyAppDelegate*)[[UIApplication sharedApplication] delegate];
[[NSNotificationCenter defaultCenter] addObserver:appDelegate
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:context];
NSError *error = nil;
if ([context hasChanges] && ![context save:&error]) {
NSLog(#"Unhandled error %#, %#", error, [error userInfo]);
abort();
}
[[NSNotificationCenter defaultCenter] removeObserver:appDelegate name:NSManagedObjectContextDidSaveNotification object:context];
}
The NSOperation main thread (well parts of it)...
- (void)main
{
//create context and retrieve NSManagedObject using the NSManagedObjectID passed in as a parameter to operation
self.message.lastSendAttempt = [NSDate date];
[self startUpload];
[self completeOperation]; //This doesn't get run because the startUpload method never returns
}
- (void)startUpload
{
[[MyLogManager sharedInstance] logSendingMessageWithURLParameters:[self.event URLParameters]]; //this is a convenience method. It just appends some extra info on the string and runs newLogWithText.
//Do some uploading stuff here...
//The operation stops before actually doing the upload when logging to CoreData.
}
The problem
My NSOperation subclass that uploads the messages (on a background thread) calls this newLogWithText function but it also updates the message it is uploading. The NSOperation uses the same methods to get and save the core-data context. (i.e. it updates the last sent date and also updates if the send was successful).
This is the first time I've tried to deal with simultaneous writes and saves to core-data.
I don't get any errors and the app carries on "working". But the operation never completes. I've tried to debug it with breakpoints but when I use breakpoints it works. Without breakpoints the operation never finishes and the upload never happens. And then it just sits there blocking the queue it is on and no other messages can be sent.
In my appDelegate (I know this isn't the ideal place for it but it's the default for a new project and I haven't changed it) the mergeChanges method is just...
- (void)mergeChanges:(NSNotification *)notification
{
[self.managedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:NO];
}
I've tried throwing the 'newLogWithText' function off to another thread and even to the main thread with no luck.
I'm just about to try it now but change the "waitUntilDone" of the merge to YES. (Just noticed this). (This didn't work).
I'm 90% certain this is down to simultaneous writes to different contexts and the conflict resolution as it is the first time I've dealt with this. If I comment out the newLogWithText function then everything works as it should.
The only alternative at the moment is to scrap the LogEntry from core data and save the logs into an array inside NSUserDefaults but that doesn't feel right. Is there another way?
EDIT
I've changed it now so it users NSUserDefaults and it works without a problem. It just feels like a hacky solution.
I am creating or opening a UIManagedDocument in my AppDelegate, using completion handler blocks to notify me when the document is ready for use.
// CHECK TO SEE IF MANAGED DOCUMENT ALREADY EXISTS ON DISK
if([fileManager fileExistsAtPath:[documentLocation path]]) {
// EXISTS BUT CLOSED, NEEDS OPENING
[[self managedDocument] openWithCompletionHandler:^(BOOL success) {
NSLog(#"DOCUMENT: Opened ...");
// TODO: Things to do when open.
}];
} else {
//DOES NOT EXIST, NEEDS CREATING AND OPENING
[[self managedDocument] saveToURL:documentLocation forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
NSLog(#"DOCUMENT: Created & Opened ...");
// TODO: Things to do when open.
}];
}
My question is I want to setup a NSFetchedResultsController on my ViewController but currently the controllers view loads before the document (from the AppDelegate) is either created or opened. I am just curious about how I inform the controller that the document is open and ready to use. My guess is I would use a NSNotification, but I just wanted to check I am not going about this the wrong way.
If you have a instance of your ViewController in appDelegate then write a public method in your ViewController and call this method in the block completion handler.
Im developing an IOS 5 app which takes a feed from a url and displays the posts in a tableview. I have a View controller that loads the table cells with the posts in the feed. This all works perfectly.
However, i wanted to use the SVProgressHUD to show whilst the feed is being loaded in a separate thread.
So in my -(void)viewDidLoad method I have the following:
- (void)viewDidLoad
{
[super viewDidLoad];
[SVProgressHUD showInView:self.view status:#"loading.." networkIndicator:YES];
dispatch_async(kBgQueue, ^{NSData* data = [NSData dataWithContentsOfURL: latestFeedURL];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:data waitUntilDone:YES];});
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(foregroundRefresh:) name:UIApplicationWillEnterForegroundNotification object:nil];
self.pull = [[PullToRefreshView alloc] initWithScrollView:(UIScrollView *) self.feedTableView];
[self.pull setDelegate:self];
[self.feedTableView addSubview:self.pull];
self.title = #"Latest";
}
- (void)fetchedData:(NSData *)responseData {
//parse out the json data
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
NSMutableArray* latestFeed = [json objectForKey:#"posts"]; //2
self.feedUpLoads = latestFeed;
NSLog(#"objects: %#", latestFeed); //3
[self.feedTableView reloadData];
[SVProgressHUD dismiss];
}
This all works fine, im getting the data which is loaded in a background thread and my table is displaying the posts with all the detail required. The problem I have is that the SVProgressHUD is not showing at all. Even if I put the [SVProgressHUD showInView line in the fetchData method, it's still not showing. (by the way i know the SVProgressHUD code works because I can actually make it show forexample in the viewWillAppear method.
Im guessing that it's not working because at the point when I'm calling it the view does not yet fully exist? But if that's the case where should I call it so that it shows whilst the feed is being called and where should I remove it?
Any help appreciated! thanks in advance!!
For anyone else having a similar problem, this can also happen because you have a long loop or a piece of code that takes a long time to execute. If this happens, your progress bar wont be shown until after the loop, which kind of defeats the purpose.
To solve this issue you need to you this:
(void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
Basically your code would look something like this:
- (IBAction)submitPost:(id)sender {
//now we show the loading bar and submit the comment
[SVProgressHUD showWithStatus:#"Submitting post" maskType:SVProgressHUDMaskTypeGradient];
SEL aSelector = #selector(submitDataOfPost);
[self performSelectorInBackground:aSelector withObject:sender];
}
This will basically load the progress bar, and in a background thread, the method you want to execute will be called. This makes sure that the UI is updated (shows the progress hud) at the same time that your code is executed.
I am successfully adding and updating records in my core data on a second thread without issue.
However, deletes don't seem to take effect until I stop and restart the App. So the delete is obviously working to a extent. I read the data before loading the tableview and don't do anything different for when there has been a deletion.
The code I'm using is
....fetch records....
BOOL deleteGem = FALSE;
if ([[attributeDict objectForKey:#"headline"] hasPrefix:#"VOID"])
deleteGem = TRUE;
if ([mutableFetchResults count] == 0) {
// not there so create a new one
if (!deleteGem) {
// so create a new one unless it needs deleting
gem = (Gem *)[NSEntityDescription insertNewObjectForEntityForName:#"Gem" inManagedObjectContext:managedObjectContext];
[gem setID:[attributeDict objectForKey:#"ID"]];
}
} else {
// already exists so either get it and then update or delete it
gem = [mutableFetchResults objectAtIndex:0];
if (deleteGem) {
// delete it if required
[managedObjectContext deleteObject:gem];
gemDeletes ++;
}
}
.....
Later on I have a method to save any updates including:
NSError *error;
if (![self.managedObjectContext save:&error]) {
....
Any ideas warmly welcomed...
Edit - with full answer based on #TechZen's answer..
Register for notifications of updates on the 2nd thread in viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:nil];
Unregister for notifications in viewDidUnload
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:nil];
Handle the update in the main thread (a new method in the view controller)
-(void)handleSaveNotification:(NSNotification *)aNotification {
[managedObjectContext mergeChangesFromContextDidSaveNotification:aNotification];
}
You have to merge the background context with the foreground context if you want the changes made in the background context to show up in the foreground context.
In the view of my application, I have a subview that says "loading," and appears while cloud data is being synced.
In viewdidload, I have
// Reachability test.
BOOL isConnected = [self checkInternet];
// If we have connectivity, then sync data with cloud.
if(isConnected){
[self beginDataSync];
}else{
[self hideLoadingView];
}
and below I have this function.
- (void)beginDataSync{
SyncData *syncData = [[SyncData alloc] init];
[syncData syncDataStart];
[self startAnimatingIndicator];
do {
[syncData dropHoodsData];
// [syncData dropBarsData];
[syncData nameDataComparison];
[syncData locDataComparison];
[syncData setLocalNamess];
[syncData setLocNamePairs];
[syncData syncDataFinish];
} while ([syncData isSyncing]);
BOOL finishedSyncing = [syncData syncDataFinish];
if (finishedSyncing) {
[self performSelector:#selector(stopAnimatingIndicator) withObject:nil afterDelay:1.0];
}
[syncData release];
}
The sync works, everything logs out correctly, and the data is correct. The buggy behavior is the view does not load the "syncing" as it syncs (application just shows splash until the data is finished syncing). This makes the app seem like it is "hanging." I want it to load the view, with the loading subview is being displayed and data syncs, then when the sync is finished, it hides the subview. Any ideas what I am doing wrong?
You need to use threading. Since you are performing all of the syncing operations in the main thread, The program loop has no opportunity to execute redrawing of the Screen.
One solution would be to start syncing after the viewDiDAppear method occurs on the loading View Controller. Otherwise I would look into NSThread.