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.
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 have a data manager (singleton class) that loads JSON from my backend service and then perform some caching in it, write it to disk, etc
so then I put this method inside a :
dispatch_sync(dispatch_queue_create("com.testing.serialQueue", DISPATCH_QUEUE_SERIAL), ^{
//my method that does disk caching here
});
inside this method when everything is done, I have a UINotification using the following:
dispatch_async(dispatch_get_main_queue(),^{
[[NSNotificationCenter defaultCenter] postNotificationName:kNewStoriesNotification object:nil userInfo:userInfo];
});
and inside the notification I do the following:
[self.collectionView performBatchUpdates:^{
if ([highlightItems count] > 0){
//doing some computation to find the index path
[weakSelf.collectionView insertItemsAtIndexPaths:indexPathArray];
}
} completion:^(BOOL finished) {
}];
and it sometimes gives me this error:
*** Assertion failure in -[UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionView.m:2662
I am not sure why.. but my guess is that it has to do with my use of GCD and notification. Any ideas?
UPDATE:
Even if I remove the insertItemsAtIndexPaths: it still crashes at performBatchUpdates.. wonder why
Why is it that everytime I call save on my NSManagedObjectContext:
-(NSManagedObjectContext*)managedObjectContext {
NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary];
NSManagedObjectContext* backgroundThreadContext = [threadDictionary objectForKey:RKManagedObjectStoreThreadDictionaryContextKey];
if (!backgroundThreadContext) {
backgroundThreadContext = [self newManagedObjectContext];
[threadDictionary setObject:backgroundThreadContext forKey:RKManagedObjectStoreThreadDictionaryContextKey];
[backgroundThreadContext release];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundThreadContext];
}
return backgroundThreadContext;
}
- (NSError*)save {
NSManagedObjectContext* moc = [self managedObjectContext];
NSError *error = nil;
#try {
if (![moc save:&error]) { //breakpoint in here
//some code
}
the app seems to be waiting forever and never got back to resume it's execution? Here's what I mean in a video. Can this be possibly caused because something is wrong with the entity/relationship model?
Here's a screenshot of the leaks instruments, I don't see any leaks, but it seems that the app is allocating something that builds up:
Have you tried ditching your multi-threading code to see if it works? My guess would be that you're mixing up threads here and accessing/saving the MOC from different threads. Managing threads manually is a PITA, you should try switching to Grand Central Dispatch.
I would also give your main MOC its own accessor so you can make sure it isn't called from background threads, and have some: - (NSManagedObjectContext*)newBackgroundMOC; and - (void)saveBackgroundMOC:(NSManagedObjectContext*)context; methods to quickly create and save MOCs from background queues/threads:
dispatch_async(my_queue, ^{
NSManagedObjectContext *context = [self newBackgroundMOC]; // create context, setup didSave notification to merge with main MOC, etc
// modify context
[self saveBackgroundMOC:context]; // main MOC gets updated
});
Migrating to GCD is a bit of work, but in the long run you'll see it's much more pleasant to work with. It goes without saying that it's also the most modern and recommended by Apple way to deal with threads.
Reposting with more concise and focused question after original question went unanswered. Also adding more insight into the problem after another day of research:
In my app delegate (didFinishLaunching), I set up a callEventHandler on CTCallCenter.
The idea is that when a callState changes, I post a notification with a userInfo dict
containing the call.callState. In my view, I observe this notification, and when the
userInfo dict contains a value of CTCallDisconnected, I want to unhide a view.
The problem I'm having is that the unhiding aspect is taking, almost consistenly, ~ 7 seconds.
Everything else is working fine, and I know this because I NSLog before and after the unhiding,
and those logs appear immediately, but the darned view still lags for 7 seconds.
Here's my code:
appDidFinishLaunching:
self.callCenter = [[CTCallCenter alloc] init];
self.callCenter.callEventHandler = ^(CTCall* call) {
// anounce that we've had a state change in our call center
NSDictionary *dict = [NSDictionary dictionaryWithObject:call.callState forKey:#"callState"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"CTCallStateDidChange" object:self userInfo:dict];
};
I then listen for this notification when a user taps a button that dials a phone number:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(ctCallStateDidChange:) name:#"CTCallStateDidChange" object:nil];
Then, in ctCallStateDidChange:
- (void)ctCallStateDidChange:(NSNotification *)notification
{
NSLog(#"121");
NSString *callInfo = [[notification userInfo] objectForKey:#"callState"];
if ([callInfo isEqualToString:CTCallStateDisconnected]) {
NSLog(#"before show");
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
NSLog(#"after show");
}
}
I've tracked the problem down to the if condition in the above code sample:
if ([[userInfo valueForKey:#"userInfo"] valueForKey:#"callState"] == CTCallStateDisconnected) {
If I simply replace that with:
if (1 == 1) {
Then the view appears immediately!
The thing is, those NSLog statements are logging immediately, but the view is
lagging in it's unhiding. How could that condition cause only part of it's block
to execute immediately, and the rest to wait ~ 7 seconds?
Thanks!
Try changing your code to this:
- (void)ctCallStateDidChange:(NSNotification *)notification
{
NSLog(#"121");
NSString *callInfo = [[notification userInfo] objectForKey:#"callState"];
if ([callInfo isEqualToString:CTCallStateDisconnected]) {
NSLog(#"before show");
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
NSLog(#"after show");
}
}
Note:
The parameter is an NSNotification, not an NSDictionary
I would not compare strings with ==
No need to cast the view to change the hidden property
Use NO instead of false
Update: Got an idea: Could you try the following, please, in between the NSLogs?
dispatch_async(dispatch_get_main_queue(), ^{
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
});
Reading the CTCallCenter doc, it seems the callEventHandler is dispatched on "the default priority global dispatch queue", which is not the main queue where all the UI stuff happens.
Looks like there is no problem with your hidden code. If I were you, I would comment out all the code after the call ends, and uncomment them one by one to see what is the problem.
Hm... try to call [yourViewController.view setNeedsDisplay] after you change hidden property. Or avoid hidden, use alpha or addSubview: and removeFromSuperview methods instead.
djibouti33,
Where you put this sentence to listen when a user taps a button that dials a phone number?on WillResignActive function?
this sentence --> [[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(ctCallStateDidChange:) name:#"CTCallStateDidChange" object:nil];
Thanks for your time,
Willy.
Reposting with more concise and focused question after original question went unanswered. Also adding more insight into the problem after another day of research:
In my app delegate (didFinishLaunching), I set up a callEventHandler on CTCallCenter.
The idea is that when a callState changes, I post a notification with a userInfo dict
containing the call.callState. In my view, I observe this notification, and when the
userInfo dict contains a value of CTCallDisconnected, I want to unhide a view.
The problem I'm having is that the unhiding aspect is taking, almost consistenly, ~ 7 seconds.
Everything else is working fine, and I know this because I NSLog before and after the unhiding,
and those logs appear immediately, but the darned view still lags for 7 seconds.
Here's my code:
appDidFinishLaunching:
self.callCenter = [[CTCallCenter alloc] init];
self.callCenter.callEventHandler = ^(CTCall* call) {
// anounce that we've had a state change in our call center
NSDictionary *dict = [NSDictionary dictionaryWithObject:call.callState forKey:#"callState"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"CTCallStateDidChange" object:self userInfo:dict];
};
I then listen for this notification when a user taps a button that dials a phone number:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(ctCallStateDidChange:) name:#"CTCallStateDidChange" object:nil];
Then, in ctCallStateDidChange:
- (void)ctCallStateDidChange:(NSNotification *)notification
{
NSLog(#"121");
NSString *callInfo = [[notification userInfo] objectForKey:#"callState"];
if ([callInfo isEqualToString:CTCallStateDisconnected]) {
NSLog(#"before show");
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
NSLog(#"after show");
}
}
I've tracked the problem down to the if condition in the above code sample:
if ([[userInfo valueForKey:#"userInfo"] valueForKey:#"callState"] == CTCallStateDisconnected) {
If I simply replace that with:
if (1 == 1) {
Then the view appears immediately!
The thing is, those NSLog statements are logging immediately, but the view is
lagging in it's unhiding. How could that condition cause only part of it's block
to execute immediately, and the rest to wait ~ 7 seconds?
Thanks!
Try changing your code to this:
- (void)ctCallStateDidChange:(NSNotification *)notification
{
NSLog(#"121");
NSString *callInfo = [[notification userInfo] objectForKey:#"callState"];
if ([callInfo isEqualToString:CTCallStateDisconnected]) {
NSLog(#"before show");
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
NSLog(#"after show");
}
}
Note:
The parameter is an NSNotification, not an NSDictionary
I would not compare strings with ==
No need to cast the view to change the hidden property
Use NO instead of false
Update: Got an idea: Could you try the following, please, in between the NSLogs?
dispatch_async(dispatch_get_main_queue(), ^{
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
});
Reading the CTCallCenter doc, it seems the callEventHandler is dispatched on "the default priority global dispatch queue", which is not the main queue where all the UI stuff happens.
Looks like there is no problem with your hidden code. If I were you, I would comment out all the code after the call ends, and uncomment them one by one to see what is the problem.
Hm... try to call [yourViewController.view setNeedsDisplay] after you change hidden property. Or avoid hidden, use alpha or addSubview: and removeFromSuperview methods instead.
djibouti33,
Where you put this sentence to listen when a user taps a button that dials a phone number?on WillResignActive function?
this sentence --> [[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(ctCallStateDidChange:) name:#"CTCallStateDidChange" object:nil];
Thanks for your time,
Willy.