I'll try to keep this brief but basically, I have an app that, in a certain mode, can near-continuously log location and other data, and snap photos (using AVFoundation) and store it all in Core Data. I discovered, as suspected, that all of this would need to be threaded...otherwise the UI gets extremely sluggish.
I have never attempted to combine Core Data with concurrency before so I read up on it as best I could. I feel like I understand what I'm supposed to do, but for someone reason it's not right. I crash with this error: "Illegal attempt to establish relationship "managedDataPoint" between objects in different contexts. I know what this means, but I thought what I have below would avoid this (I'm following what I've read)...since I get an Object ID reference from the main context, and use that to grab a new reference to the object and pass it to the "temp" context...but that isn't working as Core Data still claims I'm attempting to create a relationship across contexts (where?). Appreciate any help. Thank you!
-(void)snapPhotoForPoint:(ManagedDataPoint*)point
{
if (!_imageCapturer)
{
_imageCapturer = [[ImageCapturer alloc] init];
}
if (!_tempContext) {
_tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_tempContext.parentContext = self.managedObjectContext;
}
__block NSManagedObjectID* pointID = [point objectID];
[_tempContext performBlock:^{
NSError *error = nil;
Photo *newPhoto = [NSEntityDescription insertNewObjectForEntityForName:#"Photo" inManagedObjectContext:_tempContext];
UIImage *image = [_imageCapturer takePhoto];
newPhoto.photoData = UIImageJPEGRepresentation(image, 0.5);
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[self.managedObjectContext objectWithID:pointID];
newPhoto.managedDataPoint = tempPoint; // *** This is where I crash
if (![_tempContext save:&error]) { // I never get here.
DLog(#"*** ERROR saving temp context: %#", error.localizedDescription);
}
}];
}
Shouldn't
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[self.managedObjectContext objectWithID:pointID];
not be
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[_tempContext objectWithID:pointID];
Otherwise you are working with different contexts! Also you should check if objectID is a temporary ID and acquire a "final" one in case of.
Related
I have an entity (TestEntity) which contains a "Transformable" attribute which holds an object (MyObjectClass). On initial load, the transformable saves correctly; initialised as below:
TestEntity *test = (TestEntity *)[NSEntityDescription insertNewObjectForEntityForName:ENTITY[<Int>] inManagedObjectContext:temporaryContext];
test.transformableAttr = [[MyObjectClass alloc] initWithObject:obj];
However, when I fetch an object (I fetch as dictionary with NSDictionaryResultType) and update its "Transformable" attribute,
MyObjectClass *my_obj = ....
dict[#"transformableAttr"] = my_obj
it saves successfully but when I fetch it again I get nil for the "Transformable" attribute.
Now this only happens with "NSBatchUpdateRequest" because when I save using the MOC
TestEntity *test = ....
test.transformableAttr = updated_object
it saves successfully and I can access the updated attribute when fetched again.
Can anyone please explain? Does it mean that NSBatchUpdateRequest does not Transformable?
My NSBatchUpdateRequest code:
[context performBlock:^{
NSError *requestError = nil;
NSBatchUpdateRequest *batchRequest = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:entity];
batchRequest.resultType = NSUpdatedObjectIDsResultType;
batchRequest.propertiesToUpdate = properties;
NSBatchUpdateResult *result = nil;
SET_IF_NOT_NIL(batchRequest.predicate, predicate)
#try {
result = (NSBatchUpdateResult *)[context executeRequest:batchRequest error:&requestError];
if (requestError != nil){
// #throw
}
if ([result.result respondsToSelector:#selector(count)]){
__block NSInteger counter = [result.result count];
if (counter > 0){
[managedObjectContext performBlock:^{
for(NSManagedObjectID *objectID in result.result){
NSError *faultError = nil;
NSManagedObject *object = [managedObjectContext existingObjectWithID:objectID error:&faultError];
if (object && faultError == nil) {
[managedObjectContext refreshObject:object mergeChanges:YES];
}
counter--;
if (counter <= 0) {
// complete
}
else{
// Wait.
}
}
}];
}
else{
// No Changes
}
}
else {
// No Changes
}
}
#catch (NSException *exception) {
#throw;
}
}];
The documentation doesn't seem to call out this particular scenario, but I'm not surprised that it doesn't work. An NSBatchUpdateRequest is described as [emphasis mine]:
A request to Core Data to do a batch update of data in a persistent store without loading any data into memory.
Transformables work by converting to/from Data in memory. If your class conforms to NSCoding, the coding/decoding happens in memory, because SQLite doesn't know about NSCoding or your classes.
Your original assignment works because Core Data converts the value of transformableAttr to Data in memory and then saves the bytes of the Data to the persistent store. In the batch update, the objects aren't loaded into memory, so the transformation can't run, so the update doesn't work as you'd expect.
It's disappointing that Core Data doesn't make this clearer. Look in the Xcode console to see if it warns you about this. If it doesn't, please file a bug with Apple, because though I don't expect this to work, it's also not good for it to fail silently.
If you want to use batch updates, you'll have to convert your value in code before running the update. I'm not 100% certain how this will work but if your value conforms to NSCoding you'll start with
let transformedData: Data = NSKeyedArchiver.archivedData(withRootObject:transformableAttr)
What you do then is where I'm not sure. You might be able to use transformedData as the new value. Or you might have to access its bytes and use them somehow-- maybe using withUnsafeBytes(_:). You'll probably run into trouble because transformableAttr is not a Data, so it may get messy. It seems that batch updates aren't designed to work well with transformables.
I am trying my hand at some very basic implementation of MagicalRecord to get the hang of it and run into the following.
When I save an entry and then fetch entries of that type it will come up with the entry I just saved. However, when I save the entry, close the app, start it again, and then fetch, it comes up empty.
Code for saving:
- (void)createTestTask{
NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];
Task *task = [Task createInContext:localContext];
task.tName = #"First Task";
task.tDescription = #"First Task created with MagicalRecord. Huzzah!";
NSError *error;
[localContext save:&error];
if (error != Nil) {
NSLog(#"%#", error.description);
}
}
Code for fetching: (all I want to know here if anything is actually saved)
- (void) fetchTasks{
NSArray *tasks = [Task findAll];
NSLog(#"Found %d tasks", [tasks count]);
}
I am sure I am missing something here, but not anything I can seem to find on stackoverflow or in the Tutorials I looked at.
Any help is welcome.
I have to ask the obvious "Is it plugged in" question: Did you initialize the Core Data Stack with one of the +[MagicalRecord setupCoreDataStack] methods?
Did your stack initialize properly? That is, is your store and model compatible? When they aren't, MagicalRecord (more appropriately, Core Data) will set up the whole stack without the Persistent Store. This is annoying because it looks like everything is fine until it cannot save to the store...because there is no store. MagicalRecord has a +[MagicalRecord currentStack] method that you can use to examine the current state of the stack. Try that in the debugger after you've set up your stack.
Assuming you did that, the other thing to check is the error log. If you use
[localContext MR_saveToPersistentStoreAndWait];
Any errors should be logged to the console. Generally when you don't see data on a subsequent run of your app, it's because data was not saved when you thought you called save. And the save, in turn, does not happen because your data did not validate correctly. A common example is if you have a required property, and it's still nil at the time you call save. "Normal" core data does not log these problems at all, so you might think it worked, when, in fact, the save operation failed. MagicalRecord, on the other hand, will capture all those errors and log them to the console at least telling you what's going on with your data.
When i have started with magical record I was also facing this problem, problem is context which you are using to save data. here is my code which might help you
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *userInfoArray = [UserBasicInfo findByAttribute:#"userId" withValue:[NSNumber numberWithInt:loggedInUserId] inContext:localContext];
UserBasicInfo* userInfo;
if ([userInfoArray count]) {
userInfo = [userInfoArray objectAtIndex:0];
} else {
userInfo = [UserBasicInfo createInContext:localContext];
}
userInfo.activeUser = [NSNumber numberWithBool:YES];
userInfo.firstName = self.graphUser[#"first_name"];
userInfo.lastName = self.graphUser[#"last_name"];
userInfo.userId = #([jsonObject[#"UserId"] intValue]);
userInfo.networkUserId = #([jsonObject[#"NetworkUserId"] longLongValue]);
userInfo.userPoint = #([jsonObject[#"PointsEarned"] floatValue]);
userInfo.imageUrl = jsonObject[#"Picturelist"][0][#"PictureUrL"];
userInfo.imageUrlArray = [NSKeyedArchiver archivedDataWithRootObject:jsonObject[#"Picturelist"]];
} completion:^(BOOL success, NSError *error) {
}];
Use this when your done
[[NSManagedObjectContext MR_defaultContext]saveToPersistentStoreAndWait];
I have a tableview in my app that contains a NSFetchedResultsController to load in some CoreData objects.
As the table builds in cellForRowAtIndexPath:, for each cell I must do a fetch to get some other info from another object.
The table is filled with UserTasks, and I must get some info from a UserSite (UserTask contains a siteID attribute)
I am getting the UserSite info in a background thread, and using a temporary context. It works fine, but it still wants to lag the UI a bit when scrolling.
Site *site = [_scannedSites objectForKey:task.siteID];
if(!site)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
AppDelegate *ad = [AppDelegate sharedAppDelegate];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
temporaryContext.persistentStoreCoordinator = ad.persistentStoreCoordinator;
Site *site2 = [task getSiteWithContext:temporaryContext];
if(site2)
{
[ad.managedObjectContext performBlock:^{
Site *mainContextObject = (Site *)[ad.managedObjectContext objectWithID:site2.objectID];
[_scannedSites mainContextObject forKey:task.siteID];
}];
dispatch_async(dispatch_get_main_queue(), ^{
Site *newSite = [_scannedSites objectForKey:task.siteID];
cell.lblCustName.text = newSite.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", newSite.siteAddressLine1, newSite.siteCity, newSite.siteState];
cell.lblPhone.text = [self formatPhoneNum:newSite.phone];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
cell.lblCustName.text = #"";
cell.lblAddr.text = #"";
cell.lblPhone.text = #"";
});
}
});
}
else
{
cell.lblCustName.text = site.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", site.siteAddressLine1, site.siteCity, site.siteState];
cell.lblPhone.text = [self formatPhoneNum:site.phone];
}
As you can see, if you dont already have the UserSite info for a task in _scannedSites, a background thread gets kicked off which gets the UserSite for that task, stores it, and then on the main thread fills in the details.
Like I said there is a pretty annoying lag when scrolling... which I hoped to avoid by doing the work in the background.
Am I going about this the wrong way?
Thanks, any advice is appreciated.
EDIT
I created a relationship in CoreData and I am now using that in cellForRowAtIndexPath. If it does not exist yet, I create it. This is working much better.
Site *site = task.site;
if(!site)
{
AppDelegate *ad = [AppDelegate sharedAppDelegate];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.persistentStoreCoordinator = ad.persistentStoreCoordinator;
[temporaryContext performBlock:^{
Site *tempContextSite = [task getSiteWithContext:temporaryContext];
[ad.managedObjectContext performBlock:^{
Site *mainManagedObject = (Site *)[ad.managedObjectContext objectWithID:tempContextSite.objectID];
task.site = mainManagedObject;
NSError *error;
if (![temporaryContext save:&error])
{
}
[ad.managedObjectContext performBlock:^{
NSError *e = nil;
if (![ad.managedObjectContext save:&e])
{
}
dispatch_async(dispatch_get_main_queue(), ^{
cell.lblCustName.text = mainManagedObject.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", mainManagedObject.siteAddressLine1, mainManagedObject.siteCity, mainManagedObject.siteState];
cell.lblPhone.text = [self formatPhoneNum:mainManagedObject.phone];
});
}];
}];
}];
}
else
{
cell.lblCustName.text = site.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", site.siteAddressLine1, site.siteCity, site.siteState];
cell.lblPhone.text = [self formatPhoneNum:site.phone];
}
If UserTask relates to UserSite, the usual Core Data approach would be to create a relationship between the two and then use that relationship at run time. So, UserTask would have a property named site, and you'd just ask a specific instance for the value of that property. An ID attribute might still exist but would only be used when syncing with some external data store (like a server API).
Storing IDs and looking up objects like this is a fundamentally awkward approach that's pretty much designed to do a lot of unnecessary work at run time. It avoids all of the conveniences that Core Data tries to provide, doing things the hard way instead. Doing this work while the table is scrolling is also about the worst possible time, because it's when a performance issue will be most noticeable.
If you must do it this way for some reason, you could optimize things by looking up all of the UserSite instances in advance instead of while the table is scrolling. If you know all of the UserTask instances, go get all the sites in one call when the view loads.
It is a bad idea to send of asynchronous tasks in cellForRowAtIndexPath:. If the user scrolls there are going to be a whole bunch of threads created which are maybe not even necessary.
It would be much better to have a background process that fetches the information you want and then notifies the UI to update itself if needed. This is pretty standard stuff, you will find many examples for solid implementations easily.
I am new to the IOS programming, currently learning core data, I went into running the code where i need to save only specific objects in core data. So how can i do that?
According to the scenario, i have data from server as well as local storage (core data), but when user close the app (went to background) I want to store the data in the server(if net available) if not then in the local storage (but selected only - means specific data should be stored, there are objects which came from online server which i dont want to store on local).
Please let me know any solution if possible.
Regards
Nisar Ahmed
I see two ways to achieve this:
Iterate through inserted and updated objects and revert those you do not wont to save. Inserted objects should be deleted, updated should be refreshed:
for (NSManagedObject* obj in [self.managedObjectContext insertedObjects]) {
if (/*Shouldn't be saved*/) {
[self.managedObjectContext deleteObject:obj];
}
}
for (NSManagedObject* obj in [self.managedObjectContext updatedObjects]) {
if (/*Shouldn't be saved*/) {
[self.managedObjectContext refreshObject:obj mergeChanges:NO];
}
}
Create separate managed object context. Recreate objects that you want to save in new context and then save it.
NSManagedObjectContext* newContext = [[NSManagedObjectContext alloc] init];
[newContext setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];
for (NSManagedObject* obj in objectsWantToSave) {
NSEntityDescription* entity = [obj entity];
NSDictionary* valuesByKeys = [obj dictionaryWithValuesForKeys:[[entity attributesByName] allKeys]];
NSManagedObject* objCopy = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:newContext];
[objCopy setValuesForKeysWithDictionary:valuesByKeys];
}
[newContext save:NULL];
The second approach is better for my opinion.
Have a look into UIManagedDocument - http://developer.apple.com/library/ios/#documentation/uikit/reference/UIManagedDocument_Class/Reference/Reference.html
It takes care of a lot of the boilerplate involved in using core data.
I am trying to understand iOS Core data transient properties and am having trouble understanding some behavior.
Setup
I have two contexts a Main and a Private context. I call them mainContext and threadedContext .
The threaded context is the parent context and the main context is the child context. (I did it this way because my threaded context alters the model far more frequently than my main thread and UI do.
I have transient properties who's value I need to pass through contexts.
I find that sometimes I loose the value and sometimes I don't depending on how I run things.
Sample
This code has been simplified to show the problem. I have a Person object. The Person object has a transient entity called "other" of which you will see I assign an Other object to it that has a couple simple properties, nothing more.
- (void)case1
{
NSManagedObjectContext *mainThreadContext = [AppDelegate appDelegate].mainThreadContext;
NSManagedObjectContext *threadedContext = [AppDelegate appDelegate].threadedContext;
__block NSManagedObjectID *objectID = nil;
[mainThreadContext performBlockAndWait:^{
//create
Person *aPerson = [self createAPersonOnContext:mainThreadContext];
//setup
Other *other = [[Other alloc] init];
aPerson.other = other;
aPerson.other.favoriteColor = #"Blue";
aPerson.other.city = #"Provo";
//save
NSError *error = nil;
[mainThreadContext save:&error];
objectID = aPerson.objectID;
NSLog(#"%#",aPerson);
}];
}
When I retrieve the Object like this the person.other property is still set (note that I am saving AFTER I retrieve the object:
[threadedContext performBlockAndWait:^{
Person *aPerson = [self getPersonOnContext:threadedContext withID:objectID];
NSError *threadedError = nil;
[threadedContext save:&threadedError];
NSLog(#"threaded %#", aPerson);
}];
When I retrieve the Object like this the person.other is no longer set (note that I am saving BEFORE I retrieve the object)
[threadedContext performBlockAndWait:^{
NSError *threadedError = nil;
[threadedContext save:&threadedError];
Person *aPerson = [self getPersonOnContext:threadedContext withID:objectID];
NSLog(#"threaded %#", aPerson);
}];
I've tried different things including refreshObject:mergChanges:
I've tried to watch when objects fault but that didn't appear to be helpful.
Are transient values stored in a given context (assuming I have saved, or maybe not given the issue I am seeing) even if no model object is currently instantiated?
For those who feel they need more...
The method getPersonOnContext:WithID looks like this:
- (Person *)getPersonOnContext:(NSManagedObjectContext *)context withID:(NSManagedObjectID *)ID
{
__block Person *person = nil;
[context performBlockAndWait:^{
person = (Person *)[context objectWithID:ID];
}];
return person;
}
The createAPersonOnContext: looks like this:
- (Person *)createAPersonOnContext:(NSManagedObjectContext *)context
{
__block Person *person = nil;
[context performBlockAndWait:^{
person = (Person *)[NSEntityDescription insertNewObjectForEntityForName:#"Person"
inManagedObjectContext:context];
person.firstName = #"matt";
person.lastName = #"ZZZ";
}];
return person;
}
I just wanted to hide this code to help bring attention to the problem it self.
If you want to experiment with this I have it on github: https://github.com/mcmurrym/CoreDataBehaviors
Update:
It appears that when I save before using the ID to retrieve the object in the threaded context that it is faulting the Person object which destroys the transient values. If I retrieve the object in the threaded context before saving, the transient value is preserved because the object is not faulted.
maxpower,
Transients are quite simple. They are properties that are always non-existent in the backing store. Hence, the fact that you ever see them is because you are using a child MOC and have externally assigned those values. To ensure that a transient is always valid, you need to consider implementing the -awakeFromInsert, -awakeFromFetch, -prepareForDeletion, -didTurnIntoFault and -willTurnIntoFault methods.
Andrew