I have multiple NSInvocationOperations created and added to an NSOperationQueue. Two of these
NSInvocationOperations create lots of objects of the same parent class (Country and City which subclass Location). It has mostly gone well except that I've noticed changes to one model or the other are kinda clobbered.
Looking at the store (using a sqlite program) I see the first City (of maybe 200 total) created and then all of the Countries (again maybe 200) created. If I delete the app and run it again I'll see the first Country and then all of the Cities.
I hit the docs and noticed that Apple suggestions setting up your per thread MOCs in the start method of you NSOperation. However I'm not using an NSOperation, I'm using an NSInvocationOperation. It's actually making me question more so why they suggest creating your MOC in start.
This is my selector for my NSInvocationOperation...
+ (void)load:(NSString *)file
{
NSManagedObjectContext *managedObjectContext = [(OSSMAppDelegate *)[[UIApplication sharedApplication] delegate] adHocManagedObjectContext];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:managedObjectContext];
SBJsonParser *jsonParser = [[SBJsonParser alloc] init];
NSString *json = [[NSString alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType:#"json"]];
NSArray *objects = [[jsonParser objectWithString:json] valueForKeyPath:#"objects"];
for(NSDictionary *object in objects)
{
[self createObjectWithObject:object inManagedObjectContext:managedObjectContext];
}
NSError *error = nil;
[managedObjectContext save:&error];
}
...from the app delegate...
- (NSManagedObjectContext *)adHocManagedObjectContext
{
NSManagedObjectContext *adHocManagedObjectContext = nil;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
adHocManagedObjectContext = [[NSManagedObjectContext alloc] init];
[adHocManagedObjectContext setPersistentStoreCoordinator:coordinator];
[adHocManagedObjectContext setUndoManager:nil];
}
return adHocManagedObjectContext;
}
...then somewhere else (Note: firstRun calls load:)...
NSInvocationOperation *countryInvocationOperation = [[NSInvocationOperation alloc] initWithTarget:[Country class] selector:#selector(firstRun) object:nil];
[operationQueue addOperation:countryInvocationOperation];
Is there any problem with creating the MOC in the selector that's being invoked? I'd image it has to be since the MOC is tied to the thread it's created on. I guess any pointers as to where I'm going wrong is helpful.
I'm not sure I understand your problem (Do you have missing countries or cities?, do you have incorrect order? give an example of 'clobbered').
As for your question:
Is there any problem with creating the MOC in the selector that's being invoked?
No, there is no problem. the documentation only say it must be created ON the thread you intend to use it (start and main are methods that will run on the operation thread). hence, NSInvocationOperation will run your method in the operation thread, and you can create your MOC there without worries.
Related
I've been working on incorporating AFIncrementalStore into our app following the example code in the repository that uses an SQLite backing store. All the examples use a singleton managedObjectContext with an NSMainQueueConcurrencyType.
+ (NSManagedObjectContext *)managedObjectContext {
static NSManagedObjectContext *_managedObjectContext = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
});
return _managedObjectContext;
}
Using this moc, I'm able to perform a fetch, see it get pulled from the network, and stored in the sqlite backing store. I tried changing it to use the NSPrivateQueueConcurrencyType, and while I would see the network request, nothing was ever saved to the SQLite backing store. However, if I leave this moc with main queue concurrency, and then create a child from it, and use that moc, then everything saves fine.
+ (User *)user
{
// grab a user if we already have one
NSManagedObjectContext *managedObjectContext = [VigilCoreDatabase managedObjectContext];
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tmpContext.parentContext = managedObjectContext;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.fetchLimit = 1;
[fetchRequest setAffectedStores:#[ ]];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"User" inManagedObjectContext:tmpContext];
[fetchRequest setEntity:entity];
__block User *user = nil;
[tmpContext performBlockAndWait:^{
NSError *error = nil;
NSArray *fetchedObjects = [tmpContext executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
NSLog(#"error");
}
if(fetchedObjects.count > 0) {
user = fetchedObjects[0];
}
}];
return user;
}
I wanted to figure out if I was missing something in my understanding. I can't seem to find any examples that don't use a moc with main queue concurrency as the parent context (with the backing store using a private queue context), but at the same time can't find any documentation explaining whether this is required, or whether I need to do something to push changes to the parent manually when using a private queue context vs. having a main queue context in the stack.
At this time AFIncrementalStore suffers from a bug. I asked an SO question about another IncrementalStore (it utilizes the same code) and the response leads me to believe AFIS requires NSMainQueueConcurrencyType
Firstly, sorry if my english is not perfect, i'm not a native english person ;) !
I'm working on an Application which retrieves EKEvent From iCal and add them to my App.
The purpose of the application is a calendar so :
The user retrieve iCal's EKEvent from the calendar he wants to.
The EKEvent are saved into an Entity named "Event".
The user can edit, add, delete event from the application - the EKEvent associate will be modified in iCal too.
Issue : when the user modify something in iCal, it has to be modified into my application so the only way i found is to retrieve all EKEvent from iCal - when the app become active - and copy it into a BackUp Entity named "EventBackup". When all EKEvent from iCal are well retrieved and saved into the "EventBackup" entity i copy the entity into my main Entity "Event".
I'm doing it succesfully in async with
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^(void) { });
But I have to keep using my application - so retrieving Event * from CoreData - while i'm doing the EventBackup... problem my application crash if i'm working on the CoreData.
Could you help on that way, or propose me something different than the way i'm doing.
Thanks a lot for helping me !
You should look at NSManagedObjectContext's performBlock: method. Specifically you should create a child context, make your changes in it on a background thread and then listen for the save notification to merge it into the parent context.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:nil];
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[childContext setParentContext:self.managedObjectContext];
[childContext performBlock:^{
//Do everything here...
NSError *error = nil;
[context save:&error];
if (error) {
NSLog(#"Error saving child context:%#", error.localizedDescription);
}
}];
Listen for the child context to save and then save the main context.
- (void)managedObjectContextDidSave:(NSNotification *)notification
{
if ([notification object] != self.managedObjectContext) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self.managedObjectContext save:nil];
});
}
}
To be more clear about what i'm doing - using your code.
The methods in the block are working on the childContext's NSManagedObjectContext
I'm in the AppDelegate, that
- (void)methodToBeCalledEveryTimeTheAppBecomeActive
{
NSManagedObjectContext *contextParent = [[LavigneCoreData defaultManager] managedObjectContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:nil];
childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[childContext setParentContext:contextParent];
[childContext performBlock:^{
[self copySpecialsEvents];
[self clearBackUpEntity];
[self getCalendarWhichHasBeenSelected];
[self copyNotesIntoBackUpEntity];
[self clearEntityEvent];
[self deepCopyFromBackUpEntityToEntity];
NSError *error = nil;
[contextParent save:&error];
if (error) {
NSLog(#"Error saving child context:%#", error.localizedDescription);
}
}];
}
i'm getting the error on the line : [childContext setParentContext:contextParent];
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Parent NSManagedObjectContext must use either NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType.'
EDIT :
i fixed this error by changing
_managedObjectContext = [[NSManagedObjectContext alloc] init];
with
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
I've got 2 classes, MPRequest and MPModel.
The MPModel class has a method to lookup something from the core data store, and if not found, creates an MPRequest to retrieve it via a standard HTTP request (The method in MPModel is static and not and instance method).
What I want is to be able to get a progress of the current HTTP request. I know how to do this, but I'm getting a little stuck on how to inform the view controller. I tried creating a protocol, defining a delegate property in the MPRequest class, altering the method in MPModel to accept this delegate, and in turn passing it to the MPRequest when it is created.
This is fine, however ARC is then releasing this delegate whilst the request is running and thus doesn't do what I want. I'm trying to avoid making my delegate object a strong reference in case it throws up any reference cycles but I don't know any other way of doing this.
To start the request, from my view controller I'm running
[MPModel findAllWithBlock:^(NSFetchedResultsController *controller, NSError *error) {
....
} sortedBy:#"name" ascending:YES delegate:self]
Inside the findAllWithBlock method, I have
MPRequest *objRequest = [MPRequest requestWithURL:url];
objRequest.delegate = delegate;
[objRequest setRequestMethod:#"GET"];
[MPUser signRequest:objRequest];
[objRequest submit:^(MPResponse *resp, NSError *err) {
...
}
And in the MPRequest class I have the following property defined :
#property (nonatomic, weak) NSObject<MPRequestDelegate> *delegate;
Any ideas or suggestions?
As requested, here is some more code on how things are being called :
In the view controller :
[MPPlace findAllWithBlock:^(NSFetchedResultsController *controller, NSError *error) {
_placesController = controller;
[_listView reloadData];
[self addAnnotationsToMap];
[_loadingView stopAnimating];
if (_placesController.fetchedObjects.count > 0) {
// We've got our places, but if they're local copies
// only, new ones may have been added so just update
// our copy
MPSyncEngine *engine = [[MPSyncEngine alloc] initWithClass:[MPPlace class]];
engine.delegate = self;
[engine isReadyToSync:YES];
[[MPSyncManager sharedSyncManager] registerSyncEngine:engine];
[[MPSyncManager sharedSyncManager] sync];
}
} sortedBy:#"name" ascending:YES delegate:self];
Here, self is never going to be released for obvious reasons, so I don't see how this is the problem.
Above, MPPlace is a subclass of MPModel, but the implementation of the findAllWithBlock:sortedBy:ascending:delegate: is entirely in MPModel
The method within MPModel looks like this
NSManagedObjectContext *context = [[MPCoreDataManager sharedInstance] managedObjectContext];
[context performBlockAndWait:^{
__block NSError *error;
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([self class])];
[request setSortDescriptors:#[[[NSSortDescriptor alloc] initWithKey:key ascending:asc]]];
NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:nil
cacheName:nil];
[controller performFetch:&error];
if (!controller.fetchedObjects || controller.fetchedObjects.count == 0) {
// Nothing found or an error, query the server instead
NSString *url = [NSString stringWithFormat:#"%#%#", kMP_BASE_API_URL, [self baseURL]];
MPRequest *objRequest = [MPRequest requestWithURL:url];
objRequest.delegate = delegate;
[objRequest setRequestMethod:#"GET"];
[MPUser signRequest:objRequest];
[objRequest submit:^(MPResponse *resp, NSError *err) {
if (err) {
block(nil, err);
} else {
NSArray *objects = [self createListWithResponse:resp];
objects = [MPModel saveAllLocally:objects forEntityName:NSStringFromClass([self class])];
[controller performFetch:&error];
block(controller, nil);
}
}];
} else {
// Great, we found something :)
block (controller, nil);
}
}];
The delegate is simply being passed on to the MPRequest object being created. My initial concern was that the MPRequest object being created was being released by ARC (which I guess it probably is) but it didn't fix anything when I changed it. I can't make it an iVar as the method is static.
The submit method of the request looks like this :
_completionBlock = block;
_responseData = [[NSMutableData alloc] init];
[self prepareRequest];
[self prepareRequestHeaders];
_connection = [[NSURLConnection alloc] initWithRequest:_urlRequest
delegate:self];
And when the app starts downloading data, it calls :
[_responseData appendData:data];
[_delegate requestDidReceive:(float)data.length ofTotal:_contentLength];
Where _contentLength is simply a long storing the expected size of the response.
Got it working. It was partly an issue with threading, where the core data thread was ending before my request, me looking at the output from a different request entirely, and the way ARC handles memory in blocks.
Thanks for the help guys
I have an app which uses a UISearchBar to dynamically search from an external API based on user input.
The app is searching the external API fine and displaying results correctly, but when I select any row from the search results, the screen freezes and I am getting this error;
Tried to obtain the web lock from a thread other than the main thread or the web thread
UIKit should not be called from a secondary thread
I have absolutely no idea how I can fix this.
Here is the code;
- (void) run: (id) param {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL: [self URL]];
[parser setDelegate: self];
[parser parse];
[parser release];
[delegate parseDidComplete];
[pool release];
}
- (void) parseXMLFile: (NSURL *) url
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self setURL: url];
NSThread* myThread = [[NSThread alloc] initWithTarget:self
selector:#selector(run
object: nil];
[myThread start];
[pool release];
}
"Tried to obtain the web lock from a
thread other than the main thread or
the web thread UIKit should not be
called from a secondary thread"
The fix is conceptually simple; don't update the UI from your thread.
Assuming the parseDidComplete is where the message is sourced, then something like this will "work":
[delegate performSelectorOnMainThread: #selector(parseDidComplete) withObject: nil waitUntilDone: YES];
"Work" because threading is hard and this answer completely ignores any synchronization issues you might have.
Note that you'd be better off using NSOperation and NSOperationQueue. They are well documented and there are a bunch of examples.
I would suspect the line:
[delegate parseDidComplete];
If the delegate class is interacting with UIKit components, then the background thread that is retrieved the XML contents is then calling the front-end objects which must all be in the main thread.
You may want to look at using an NSOperation and NSOperationQueue to do the asynchronous operations. I believe that provides a more threadsafe way to handle this type of use case.
I have come across an interesting conundrum (of course, I could just being doing something horribly wrong).
I would like an NSTokenField to "represent" a relationship in a Core Data Application. The premise is such: You click on a Note from a TableView (loaded from the Notes Array Controller). The token field is then bound (through "value") to the Notes Array Controller selection.Tags. Tags is a to-many relationship on the entity Notes.
Obviously, an NSTokenField will not accept the NSSet that the Array Controller Provides it. To get around this, I subclassed NSTokenFieldCell and overrode its objectValue and setObjectValue: methods. I thought that I could simply translate the NSSet that was being provided to the NSArray that the NSTokenFieldCell expected. (Note: I originally tried overriding these methods on a NSTokenField subclass; however, they were not being called.)
So, I came up with said code:
- (void)setObjectValue:(NSSet*)object {
tagsList = [object copy];
NSMutableArray *displayList = [[NSMutableArray alloc] init];
for (id newObject in tagsList) {
[displayList addObject:[newObject valueForKey:#"Name"]];
}
[super setObjectValue:displayList];
}
- (id)objectValue {
NSArray *displayList = [super objectValue];
NSEntityDescription *tagEntity = [NSEntityDescription
entityForName:#"Tag"
inManagedObjectContext:[appDelegate
managedObjectContext]];
NSMutableSet *returnValue = [[NSMutableSet alloc] init];
for (NSString *token in displayList) {
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:tagEntity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"Name == %#", token];
[request setPredicate:predicate];
NSError *error;
NSArray *results = [[appDelegate managedObjectContext] executeFetchRequest:request error:&error];
if (results == nil) {
NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:[appDelegate managedObjectContext]];
[object setValue:token forKey:#"Name"];
[returnValue addObject:object];
} else {
[returnValue addObject:[results objectAtIndex:0]];
}
}
return returnValue;
}
It crashes. :( And, surprisingly it crashes on the line that calls [super objectValue]. It gives me the error:
-[NSConcreteAttributedString countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance ...
Sigh. The sad thing is that when I go into the Core Data XML file and give the Note a Tag, it displays correctly, and [super setObjectValue:] is passed an array of strings. However, as soon as I enter something else and mouse away, I get the error.
I am not sure what to do about this. Can anyone spot anything horribly wrong with this? Thanks.
UPDATE:
If it makes a difference, I do not have a delegate configured for the TokenField.
In typical SO fashion, I found the answer to my own question. It was silly to begin with. I simply needed another ArrayController bound to the Notes selection.Tags set. Then, I bound the NSTokenField to the ArrangedObjects of that Controller, implemented some delegate methods. Boom. Simple.
Silly me.