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.
Related
SETUP (You can read this later and skip to the scenario section first)
It's an old app, with manually setup CoreData stack like this:
+ (NSManagedObjectContext *)masterManagedObjectContext
{
if (_masterManagedObjectContext) {
return _masterManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self createPersistentStoreCoordinator];
if (coordinator != nil) {
_masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_masterManagedObjectContext.retainsRegisteredObjects = YES;
_masterManagedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_masterManagedObjectContext.persistentStoreCoordinator = coordinator;
}
return _masterManagedObjectContext;
}
+ (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext) {
return _managedObjectContext;
}
NSManagedObjectContext *masterContext = [self masterManagedObjectContext];
if (masterContext) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.retainsRegisteredObjects = YES;
_managedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_managedObjectContext.parentContext = masterContext;
}
return _managedObjectContext;
}
+ (NSManagedObjectContext *)newManagedObjectContext
{
__block NSManagedObjectContext *newContext = nil;
NSManagedObjectContext *parentContext = [self managedObjectContext];
if (parentContext) {
newContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
newContext.parentContext = parentContext;
}
return newContext;
}
And then save context recursively:
+ (void)saveContext:(NSManagedObjectContext *)context
{
[context performBlockAndWait:^{
if (context.hasChanges && context.persistentStoreCoordinator.persistentStores.count) {
NSError *error = nil;
if ([context save:&error]) {
NSLog(#"saved context: %#", context);
// Recursive save parent context.
if (context.parentContext) [self saveContext:context.parentContext];
}
else {
// do some real error handling
NSLog(#"Could not save master context due to %#", error);
}
}
}];
}
SCENARIO
The app load lots of data from a server, then perform update inside newContext first, then merge into mainContext -> masterContext -> persistentStore.
Because lots of data, the sync process has been divided into about 10 async threads => we have 10 newContext at a time.
Now, the data is complicated, with things like parents <-> children (same class). 1 parent can have many children, and a child can have a mother, father, god father, step mother..., so it's n-n relationship. First, we fetch parent, then perform fetch child and then set the child to parent, and so on.
The server is kinda stupid, it can't send disabled objects. However the customer would like to control the display of app's objects from the back end, so I have 2 properties to do that:
hasUpdated: At the beginning of loading process, perform a batch update, set all object's hasUpdated to NO. When got data from the server, update this property to YES.
isActive: When all loading was done, perform batch update this property to NO if hasUpdate == NO. Then, I have a filter that won't show object with isActive == NO
ISSUE
Customers complain why some objects being missing even if they're enable in the backend. I've struggle and debugging for so long after got to this strange issue:
newContext.updatedObjects : { obj1.ID = 100, hasUpdated == YES }
"saved newContext"
mainContext.updatedObjects: {obj1.ID = 100, hasUpdated == NO }
// I'll stop here. Obviously, master got updated = NO and finally isActive will set to no, which cause missing objects.
If it happened every time, then probably easier to fix (¿maybe?). However, it occurs like this:
First time running (by first time, I mean app start from where appDidFinishLaunch... got called): all correct
2nd time: missing (153 objects)
3rd time: all correct
4th time: missing (153 objects) (again? exactly those with multiple parents, I believe so!)
5th time: correct again
... so on.
Also, it looks like this happened for objects which have the same context (same newContext). Unbelievable.
QUESTIONS
Why is this happening? How do I fix this? If those objects don't have children, my life would be easier!!!!
BONUS
In case you'd like to know how the batch update is, it's below. Note:
Download requests are in async queue: _shareInstance.apiQueue = dispatch_queue_create("product_request_queue", DISPATCH_QUEUE_CONCURRENT);
Parse response and update properties are syncronous in a queue: _shareInstance.saveQueue = dispatch_queue_create("product_save_queue", DISPATCH_QUEUE_SERIAL);
Whenever parse complete, I perform save newContext and call for updateProductActiveStatus: in the same serial queue. If all requests are finished, then perform batch update status. Since request are done in concurent queue, it's always finished earlier than save (serial) queue, so it's pretty much fool proof process.
Code:
// Load Manager
- (void)resetProductUpdatedStatus
{
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.propertiesToUpdate = #{ #"hasUpdated" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update hasUpdated: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
[[CoreDataUtil managedObjectContext] performBlockAndWait:^{
[[CoreDataUtil managedObjectContext] refreshAllObjects];
}];
}];
}
- (void)updateProductActiveStatus:(SyncComplete)callback
{
if (self.apiRequestList.count) return;
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.predicate = [NSPredicate predicateWithFormat:#"hasUpdated = NO AND isActive = YES"];
request.propertiesToUpdate = #{ #"isActive" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update isActive: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
NSManagedObjectContext *maincontext = [CoreDataUtil managedObjectContext];
NSLog(#"Refreshed master");
[maincontext performBlockAndWait:^{
[maincontext refreshAllObjects];
NSLog(#"Refreshed main");
// Callback
if (callback) dispatch_async(dispatch_get_main_queue(), ^{ callback(YES, nil); });
}];
}];
}
mergePolicy is evil. The only correct mergePolicy is NSErrorMergePolicy any other policy is asking core-data to silently fail and not update when you expect it too.
I suspect that your problem is that you are writing simultaneously to core-data with the background contexts. (I know that you say you have a serial queue - but if you call performBlock inside the queue then each block is executed simultaneously). When there is a conflict stuff gets overwritten. You should only write to core-data in one synchronous way.
I wrote an answer on how to accomplish this with a NSPersistentContainer:
NSPersistentContainer concurrency for saving to core data and I would suggest that you migrate your code to it. It really should not be that hard.
If you want to keep the code as close to what is currently is as possible that also is not that hard.
Make a serial operation queue:
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
And do all writing using this queue:
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = [CoreDataUtil newManagedObjectContext];
[context performBlockAndWait:^{
blockCopy(context);
[CoreDataUtil saveContext:context];
}];
}]];
}
Also it could be that the objects ARE updated, but you aren't seeing it because you are relying on a fetchedResultsController to be updated. And fetchedResultsController don't update from batch update requests.
I have two UIViewControllers in a Tab Bar
In one of the TabBar I am making an api call using AFNetworking and this api call is saving data in CoreData.
Here is my code
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < cartList.count; i++)
{
NSDictionary *dict = [cartList objectAtIndex:i];
NSFetchRequest *request = [Orders fetchRequest];
request.predicate = [NSPredicate predicateWithFormat:#"orderId = %#", [dict objectForKey:kiD]];
NSError *error = nil;
NSArray *itemsList = context executeFetchRequest:request error:&error];
if (itemsList.count == 0)
{
Orders *order = [NSEntityDescription insertNewObjectForEntityForName:#"Orders" inManagedObjectContext:appDel.persistentContainer.viewContext];
[order updateWithDictionary:dict];
order.isNew = NO;
}
else
{
Orders *order = [itemsList objectAtIndex:0];
[order updateWithDictionary:dict];
order.isNew = NO;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
[appDel saveContext];
[self refreshValues:NO];
});
});
In second VIewController I am doing something like that. If I switch the tab controllers very fast the app crashes at
[appDel saveContext];
most probably because the last time viewContext was used by other UIviewController in Background thread.
What is the work around I can adopt to fix this problem
If this is correctly implemented
[appDel.persistentContainer performBackgroundTask:^(NSManagedObjectContext * _Nonnull context)
{
NSFetchRequest *request = [Categories fetchRequest];
NSBatchDeleteRequest *deleteReq = [[NSBatchDeleteRequest alloc] initWithFetchRequest:request];
NSError *deleteError = nil;
[appDel.persistentContainer.viewContext executeRequest:deleteReq error:&deleteError];
for (int i = 0; i < dataArr.count; i++)
{
Categories *category = [NSEntityDescription insertNewObjectForEntityForName:#"Categories" inManagedObjectContext:appDel.persistentContainer.viewContext];
[category updateWithDictionary:[dataArr objectAtIndex:i]];
}
#try {
NSError *error = nil;
[context save:(&error)];
} #catch (NSException *exception)
{
}
[self getCategoryItems];
}];
Core-data is not thread-safe, neither for reading for for writing. If you violate this ever core-data can fail in unexpected ways. So even if it appears to work you can find core-data suddenly crashing for no apparent reasons. In other words, accessing core-data from the wrong thread is undefined.
There are a few possible solutions:
1) only use the main thread for reading and writing to core-data. This is an OK solution for simple apps that don't do a lot of data import or export and have relatively small data sets.
2) Wrap NSPersistentContainer's performBackgroundTask in an operation queue and only write to core-data through that method and never write to the viewContext. When you use performBackgroundTask the method gives you a context. You should use the context to fetch any objects that you need, modify them, save the context and then discard the context and the objects.
If you try to write using both performBackgroundTask and writing directly to the viewContext you can get write conflicts and lose data.
Create a child NSManagedObjectContext object with NSPrivateQueueConcurrencyType your data processing in background queue.
Read Concurrency guide for more info.
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.
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];
Part of my UITableViewCell's content creation is delayed by the fault that happens on one object's (CoreData NSManagedObject) initial access. This manifests itself in a small hiccup the cell is first scrolled into view. I decided to push that access of those objects off to a background thread.
This is how I implemented it and it works well, but we all know that we are not supposed to access one thread(the main thread)'s NSManagedObjectContext in another thread, but can we get the objectID of an object in a second thread if it was originally fetched in the first thread?
Getting the objectID takes a small amount of time, which I was hoping to push into the background with everything else.
MyRecord *record = [self.frc objectAtIndexPath: indexPath];
// Should the following be here or can it be below in the background thread?
// NSManagedObjectID *recordObjectID = record.objectID;
dispatch_async(_recordViewQueue, ^(void) {
if ([cell.origIndexPath isEqual:indexPath]) {
// should the following be here or above? It works here, but am I just lucky?
// this call seems to take about 2/100 of a second
NSManagedObjectID *recordObjectID = record.objectID;
NSManagedObjectContext *bgndContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
bgndContext.persistentStoreCoordinator = App.sharedApp.storeCoordinator;
MyRecord *newRecord = (MyRecord *) [bgndContext objectWithID:recordObjectID];
[self updateCell:cell withRecord:newRecord];
if ([cell.origIndexPath isEqual:indexPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
[(UIView*) cell.recordView setNeedsDisplay];
});
}
}
});
Is this safe? Or do I have to get the objectID in the mainThread?
It is safe to pass the objectID of a managed object between threads. It is not safe to use a managed object between threads. Use the objectID and your thread's managed object context to call existingObjectWithID:error: to get an instance of the managed object for that thread.
I would update your code like so:
MyRecord *record = [self.frc objectAtIndexPath: indexPath];
NSManagedObjectID *recordObjectID = record.objectID;
dispatch_async(_recordViewQueue, ^(void) {
if ([cell.origIndexPath isEqual:indexPath]) {
NSManagedObjectContext *bgndContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
bgndContext.persistentStoreCoordinator = App.sharedApp.storeCoordinator;
NSError * error = nil;
MyRecord *newRecord = (MyRecord *) [bgndContext existingObjectWithID:recordObjectID error:&error];
if (newRecord) {
[self updateCell:cell withRecord:newRecord];
if ([cell.origIndexPath isEqual:indexPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
[(UIView*) cell.recordView setNeedsDisplay];
});
}
}
else {
NSLog(#"unable to find existing object! error: %# (userInfo: %#)", [error localizedDescription], [error userInfo]);
}
}
});