I'm making some app and I want to provide offline functionality to it.
Problem is with getting new data from backend as temporary objects not saved in persistent store. Why I want this? Because I want to check whether data from backend is newer than offline one (by update date) If yes then update, otherwise, send it to the backend.
For now I'm trying something like this:
NSMutableURLRequest *apiEmailRequest = [[RKObjectManager sharedManager] requestWithObject:#"ApiEmail" method:RKRequestMethodGET path:pathToContent parameters:nil];
RKObjectRequestOperation *apiEmailOperation = [[RKObjectManager sharedManager] managedObjectRequestOperationWithRequest:apiEmailRequest managedObjectContext:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
*********************CHECK FOR BACKEND EMAILS AND OFFLINE ONE **********************
NSArray *backendEmails = [mappingResult array];
for (ApiEmail *backendEmail in backendEmails) {
if ([backendEmail isKindOfClass:[ApiEmail class]]) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"ApiEmail"];
NSPredicate *filterByApplication = [NSPredicate
predicateWithFormat:#"emailId == %#", backendEmail.emailId];
[fetchRequest setPredicate:filterByApplication];
NSArray *persistentEmails = [[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil];
*HERE PUT IT INTO mainQueueManagedObjectContext and
saveToPersistentStore else POST it to the backend*
}
}
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
*ERROR*
}];
return apiEmailOperation;
[[RKObjectManager sharedManager] enqueueObjectRequestOperation:apiEmailOperation];
Is there any way to do it without creating new RKObjectManager?
Best regards, Adrian.
UPDATE
-(void)willSave {
[super willSave];
NSDictionary *remoteCommits = [NSDictionary dictionaryWithDictionary:[self committedValuesForKeys:#[#"updateDate"]]];
NSDate *updateDate = [remoteCommits valueForKey:#"updateDate"];
NSComparisonResult result = [self.updateDate compare:updateDate];
if(result == NSOrderedDescending) {
[self.managedObjectContext refreshObject:self mergeChanges:NO];
} else {
[self.managedObjectContext refreshObject:self mergeChanges:YES];
}
}
After such modification I'm getting Failed to process pending changes before save. The context is still dirty after 1000 attempts.
The below is unlikely to work in your situation actually, specifically because of the way discardsInvalidObjectsOnInsert works.
You may be able to do this by following the below process but additionally implementing willSave on the managed object and checking the status there. If you decide to abandon the updates you can try using refreshObject:mergeChanges: with a merge flag of NO.
With KVC validation you have 2 options:
edit individual pieces of data as it is imported
abandon the import for a whole object
Option 2. requires that you use the Core Data validation to prevent the import. That means doing something like making the date stamp on the object non-optional (i.e. required) and in your KVC validation setting it to nil when you determine that the import should be aborted.
Note that for this to work you need to set discardsInvalidObjectsOnInsert on the entity mapping.
After big help from #Wain, I finally got it working. Without this brave men I would still be in the sandbox. Solution:
-(BOOL)validateUpdateDate:(id *)ioValue error:(NSError **)outError {
NSComparisonResult result = [self.updateDate compare:(NSDate *)*ioValue];
if (result == NSOrderedDescending) {
self.updateDate = nil;
return NO;
}
return YES;
}
-(void)willSave {
[super willSave];
if (self.updateDate == nil) {
[self.managedObjectContext refreshObject:self mergeChanges:NO];
}
}
Thank You so much for your time and help.
Best regards, Adrian.
Related
I am working on an app which makes use of Core Data. I am using parent/child contexts to access Core Data from both the main and background threads. The application makes use of Quickblox framework (don't bother if you aren't aware of this framework, you need not be aware of it to answer the question), I have to use the API of this framework which makes a call to a server, fetches data and hands the control back to me using a block. Control is always returned back on the main thread i.e the block always executes on the main thread.
Here's my code:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
QBResponsePage *page = [QBResponsePage responsePageWithLimit:ResponseLimit skip:0];
[QBRequest messagesWithDialogID:weakSelf.dialog.id
extendedRequest:extendedRequest
forPage:page
successBlock:^(QBResponse *response, NSArray *messages, QBResponsePage *page)
{
if(messages.count > 0)
{
NSArray *filteredArray=nil;
NSFetchRequest *request=[[NSFetchRequest alloc] initWithEntityName:#"Dialog"];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"id == %#",weakSelf.dialog.id];
[request setPredicate:predicate];
NSArray *results=[weakAppDelegate.managedObjectContext executeFetchRequest:request error:nil];
Dialog *parentDialog=nil;
if([results count]>0){
parentDialog=[results objectAtIndex:0];
}
for(QBChatMessage *message in messages)
{
NSFetchRequest *request=[[NSFetchRequest alloc] initWithEntityName:#"Chats"];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"chat_Id == %#",message.ID];
[request setPredicate:predicate];
NSArray *results=[weakAppDelegate.managedObjectContext executeFetchRequest:request error:nil];
if([results count]==0){
if([message.dateSent compare:parentDialog.last_Message_Date]==NSOrderedDescending){
parentDialog.last_Message_Date=message.dateSent;
parentDialog.last_Message_User_Id=[NSNumber numberWithInteger:message.senderID];
parentDialog.lastMessage=message.text;
}
Chats *recievedChat=[NSEntityDescription insertNewObjectForEntityForName:#"Chats" inManagedObjectContext:weakAppDelegate.managedObjectContext];
recievedChat.sender_Id=[NSNumber numberWithInteger:message.senderID];
recievedChat.date_sent=message.dateSent;
recievedChat.chat_Id=message.ID;
recievedChat.belongs=parentDialog;
recievedChat.message_Body=message.text;
}
}
//commit main context
}
//update user profilepic
} errorBlock:^(QBResponse *response) {
//handle error
}];
});
Everything works fine, but as soon as the statement recievedChat.belongs=parentDialog; gets executed, dealloc never gets called even after popping out the view controller containing code. Commented out this statement, obviously Core Data relationships won't work but dealloc gets called as expected.
I am confused. Please help guys.
Thanks in advance.
I am using RestKit 0.2.x with Core Data and following the standard tutorials, ie:
Create Core Data model and use mogenerator to make code
Instantiate object manager with base URL
Create managed object context and persistent store
Create entity mappings for all entities returned by my web service
Create response descriptors for all web service endpoints and entities
Add response descriptor to object manager
Everything seems to be "working" just fine ... I can call
[[RKObjectManager sharedManager] getObjectsAtPath:_requestPath parameters:_requestParameters success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self requestSuccess];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
[self requestError:error];
}];
... all day long, and I then I keep handling with (as shown in the tutorials)
- (void)requestSuccess {
NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:_entityName];
fetchRequest.sortDescriptors = #[_defaultSortDescriptor];
NSError *error = nil;
requestData = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
[_delegate handleRequestSuccess:self withData:requestData];
//[self cleanupRequestBeforeSuccessWithData:requestData];
[self completeRequest];
}
Now the problem is that at least by default, RestKit+CoreData actually persists your GET'ted objects to its own persistence store, or something like that. I'll explain the "cleanupRequest..." in a moment.
That kind of defeats the purpose of trying to allow the users to specify parameters at the level of the web service client, because all of the objects seem to end up in the same place anyway.
For instance, let's say I have a method /api/endpoint?queryString and I call it with two different sets of parameters:
[[RKObjectManager sharedManager] getObjectsAtPath:#"/api/endpoint" parameters:PARAMS_ONE success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self requestSuccess];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
[self requestError:error];
}];
[[RKObjectManager sharedManager] getObjectsAtPath:#"/api/endpoint" parameters:PARAMS_TWO success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self requestSuccess];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
[self requestError:error];
}];
If I then blindly follow the tutorials about how to retrieve my objects, my callbacks are then identical!
NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"EndpointDataTransferObject"];
fetchRequest.sortDescriptors = #["endpointDataTransferObjectID"];
NSError *error = nil;
requestData = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
The result, of course, is that the my delegate gets sent (pseudocode) requestData WHERE PARAMS_ONE on the first call, and then requestData WHERE PARAMS_ONE UNION requestData WHERE PARAMS_TWO on the second call.
Now all I really want is to be able to conduct the NSFetchRequest on only those items mapped from the web service. I think this is a totally reasonable expectation, so clearly I am missing something because whoever wrote this library is much more clever than I.
For instance, if I could somehow get an NSArray of all the objects from the two parameters it provides in the success block (RKRequestRequestOperation *o, RKMappingResult *m) - and if I can, please tell me how!!! - then my problem would be solved, and I could enjoy the caching without having to worry about whether my filters are being ignored.
What I do not want to do, however, is this:
Call getObjectsAtPath: parameters: success: failure: with parameters and/or path representing a sort of "server-side" predicate
On success, create a NSFetchRequest and a client-side predicate that mirrors my server-side predicate
This approach seems really really dumb, and yet, I don't know any better. But I refuse to do that. It is error-prone, redundant, and potentially resource-intensive.
So instead, I've opted to add a little method cleanupRequestBeforeSuccessWithData at the end of my success callback before calling completion:
- (void)cleanupRequestBeforeSuccessWithData:(NSArray *)managedObjects {
NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
for (NSManagedObject *o in managedObjects) {
[managedObjectContext deleteObject:o];
}
NSError *error = nil;
[managedObjectContext save:&error];
}
This is ugly but it sure gets the job done. Now it totally empties the cache, but I'd rather have to make requests over and over again than to form "server-side" predicates with URL's and then form client-side NSPredicates.
What am I missing about how this is supposed to work? Clearly, I'm missing something big.
If I then blindly follow the tutorials
Never a good idea, you need to take what you've learnt from the tutorials and apply it to your problem space.
The items are indeed persisted as you're using Core Data. You don't need to but it does help with memory management. Technically you don't need to run a fetch because the mapping result (RKMappingResult) contains all the mapped objects, so you can just extract them and pass them on.
The other approach you shun of running a local fetch with filters is actually perfectly acceptable, and I'd say it's the usual approach as it's how a fetched results controller based approach works... To facilitate that you would add the query parameters to your mapping so that the mapped objects are updated. You do need to be cautious though as multiple requests returning the same objects could overwrite the data (assuming you're using unique identifiers).
Im still trying to figure out what loads the UI thread. In a class(a child of UITableView) there's a FRC:
NSFetchRequest *request = [DEPlace MR_requestAllWithPredicate:[NSPredicate predicateWithFormat:#"isWorking == YES"]];
request.sortDescriptors = #[ [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES] ];
self.placesController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:[NSManagedObjectContext MR_rootSavingContext]
sectionNameKeyPath:nil
cacheName:nil];
self.placesController.delegate = self;
It used to be attached to a MR_contextForCurrentThread. Changing it to rootSavingContext slightly affected the performance. Then i set both root and default contexts to the same one:
[NSManagedObjectContext MR_setRootSavingContext:managedObjectStore.persistentStoreManagedObjectContext];
[NSManagedObjectContext MR_setDefaultContext:managedObjectStore.persistentStoreManagedObjectContext];
Default context used to be set to mainQueueManagedObjectContext. I want to move literally everything core data related to background and let FRC take care of interactions with the UI. FRC delegate gets new data by:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
//self.places = [self sortPlaces:controller.fetchedObjects];
self.places = controller.fetchedObjects;
[self.delegate contentUpdatedInDatasource:self];
}
I disabled the sorting by now, thought it could affect the main thread. I've tried figuring out what else could load the main thread with Time Profiler, but didn't find anything suspicious. screenshot
When all the data is loaded everything run smoothly, the app lags only at the first start, when the DB gets populated. Since everything loading-related is held by RestKit i don't think it causes problems.
I was thinking of delaying requests by 10 per second max, but have no idea how can i achieve it. Basically, on the start app gets and array of IDs(~250 by now) and then looping trough the array and requesting data from the server by each ID. It's not so crucial so far, but when the array grow up to 1-2k it would be a big problem. Btw, a single data object has 4 relationships in the DB. Is reducing dependencies a possible solution?
UPDATE:
I've tried to split the request to 1 by 1 and it caused a pretty weird behaviour.
For some reason there's a huge delay between requests.
This is how i get an array of IDs
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[APIRoot stringByAppendingFormat:#"/venues/listId?%#=%#&%#=%#", TokenKey, [DEUser token], UDIDKey, [DEUser udid]]]]];
// dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_queue_t backgroundQueue = dispatch_queue_create("com.name.bgqueue", NULL);
op.successCallbackQueue = backgroundQueue;
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//gettin an array of IDs
NSArray *array = (NSArray*) responseObject;
if(array.count)
{
_array = array;
[self getVenuesFromSelfArrayWithCurrentIndex:0];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"3rr0r: %#", error);
}];
[[NSOperationQueue mainQueue] addOperation:op];
And this is a code of recursive method:
- (void)getVenuesFromSelfArrayWithCurrentIndex: (NSUInteger)index
{
if(index >= _array.count){ NSLog(#"loading finished!"); return; }
//version of the app, location e.t.c.
NSMutableDictionary *options = [[self options] mutableCopy];
[options setObject:[_array objectAtIndex:index] forKey:#"venueId"];
//method below calls RKs getObjectsAtPath, and it's pretty much the only thing it does
[[DEAPIService sharedInstance] getObjectsOfClass:[DEPlace class]
withOptions:options
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult){
NSManagedObject *object = [mappingResult.array firstObject];
if([object isKindOfClass:[DEPlace class]])
{
[self getVenuesFromSelfArrayWithCurrentIndex:index+1];
}
} failure:^(RKObjectRequestOperation *operation, NSError *error){
NSLog(#"Failed to load the place with options: %#", options.description);
[self getVenuesFromSelfArrayWithCurrentIndex:index+1];
}];
}
The weird part is that it takes ~1-2 seconds(!) to start next request and cpu usage log and threads look.. strange.
Screenshot 1
Screenshot 2
Any suggestions?
At this point in time I can only suggest around the 250 requests. You can't make more than around 4 or 5 concurrent network requests without flooding the network and grinding it to a halt on a mobile device. Really you should change the web service design so you can send batch requests as this is a lot more efficient both for the client and the server.
Anyway, you can limit the concurrent requests by setting the maxConcurrentOperationCount of the operationQueue of your object manager. The recommendation would be to set it to 4.
I have the following fetch request block set up to deal with deletion of orphaned objects:
[objectManager addFetchRequestBlock:^NSFetchRequest *(NSURL *URL) {
RKPathMatcher *pathMatcher = [RKPathMatcher pathMatcherWithPattern:API_GET_ACTIVE_RIDES];
NSString * relativePath = [URL relativePath];
NSDictionary *argsDict = nil;
BOOL match = [pathMatcher matchesPath:relativePath tokenizeQueryStrings:NO parsedArguments:&argsDict];
if (match) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Ride"];
return fetchRequest;
}
return nil;
}];
Since I need to allow users to log out and log back in, I clear all data from core data using the following:
+ (void) clearUserData {
NSError * error = nil;
[[RKManagedObjectStore defaultStore] resetPersistentStores:&error];
if(error != nil){
[WRUtilities criticalError:error];
return;
}
}
However, if I log out and log back into my app, objects that were loaded the first time I logged in are not loaded from the server. Using RestKit logging, I can see that the request goes out and returns the correct data from the server, but mapping appears to be completely skipped, causing no objects to be (re)inserted into core data.
If I remove my fetch request block, everything works as I would expect - clearUserData removes all data, and upload login the data is re-queried from the server and reloaded into core data.
My question is two fold. What do I need to change to get the expected behavior of successfully reloading data, and why does the fetch request block, which I understand to be only for deleting orphaned objects, have an effect on this scenario?
I've seen this before and just removed the fetch request block, but I would prefer to use this feature rather than skip it because of this problem.
I had this exact problem and managed to find a solution. Essentially the code below will only return a fetch request if there are existing items held in CoreData. The problem with returning it all the time means that RestKit will delete the items you get from the server the first time. The idea here is to return nil if there are no items currently held in CoreData. Not sure if this is the way RestKit would recommend, but it worked for me.
RKObjectManager *sharedManager = [RKObjectManager sharedManager];
[sharedManager addFetchRequestBlock:^NSFetchRequest *(NSURL *URL) {
RKPathMatcher *userPathMatcher = [RKPathMatcher pathMatcherWithPattern:#"my/api/path"];
BOOL match = [userPathMatcher matchesPath:[URL relativePath] tokenizeQueryStrings:NO parsedArguments:nil];
if(match) {
// lets first check if we have anything in coredata
NSString * const entityName = NSStringFromClass([MyCoreDataEntity class]);
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName];
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
if(error) {
// this shouldn't happen, but if something went wrong querying CoreData
// lets just save the new ones
NSLog(#"error %#", error);
return nil;
}
if([results count] > 0) {
// if we already have something saved, lets delete 'em and save
// the new ones
return [NSFetchRequest fetchRequestWithEntityName:entityName];
}
else {
// if we don't have something saved, lets save them
// (e.g. first time after login after we cleared everything on logout)
return nil;
}
}
return nil;
}];
Hope that helps!
What are the best practices on where to update my Core Data?
The first guy who worked on this project I'm working right now created all the Core Data related functions inside the ViewController, but I wanted to declare them inside the model classes (NSManagedObject subclass) to separate concerns.
The main function is a AFNetworking postPath that calls a web service and returns an array of objects to add/edit/delete. What I did was create a class method and do this AFNetwork call inside it:
+ (void)updateEbooksListWithSuccessBlock:(void (^)())successBlock andFailureBlock:(void (^)())failureBlock {
NSURL *url = urlSchema (urlWebServices, #"");
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSString *postPath = [NSString stringWithFormat:#"ws-ebooks-lista.php"];
[httpClient postPath:postPath parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([operation isKindOfClass:[AFHTTPRequestOperation class]]) {
NSDictionary *result = [[responseObject objectFromJSONData] retain];
bool success = statusDoRetornoDoWebService(result); //Function that checks if the return was successful
//Configura o Core Data
NSError *error = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSManagedObjectContext *localManagedObjectContext = [[NSManagedObjectContext alloc] init];
[localManagedObjectContext setParentContext:[(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext]];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Ebooks" inManagedObjectContext:localManagedObjectContext];
NSPredicate *filterPredicate;
[request setEntity:entity];
if (success) {
NSArray *ebookInfos = [result objectForKey:#"saida"];
Ebooks *ebook;
NSManagedObject *objectInsert;
for (NSDictionary* ebookInfo in ebookInfos) {
filterPredicate = [NSPredicate predicateWithFormat:#"ebooks_id == %#",[ebookInfo valueForKey:#"id_ebook"]];
[request setPredicate:filterPredicate];
ebook = [[localManagedObjectContext executeFetchRequest:request error:&error] lastObject];
objectInsert = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:localManagedObjectContext];
if (ebook) {
if (![[ebookInfo valueForKey:#"excluido"] isEmpty]) {
//Delete Ebook
} else {
//Update Ebook
}
} else {
//Add Ebook
}
if (![localManagedObjectContext save:&error]) {
//Log Error
}
[objectInsert release];
}
}
[request release];
[localManagedObjectContext release];
}
[successBlock invoke];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//Failure
[failureBlock invoke];
}];}
And it works fine while the app is running, but if I close it (through Xcode) and open it again, the changes aren't saved. I tried not using the "parent context" way and just using the AppDelegate managed object context (since AFNetworking callbacks always runs on the main queue) but no success: the data is not persisted. Why is that? Am I doing something wrong? Is it bad practice? Should I leave everything in the View Controller the way it was?
Thanks!
I think it is a bad idea to have too much logic that ultimately relates to your data model into your entity classes. These tasks simply do not belong there. The entity classes should focus only on what they encapsulate: the entity instances themselves.
To illustrate: think of a class that represents a number (like NSNumber). It think it is not convenient to extend it to give you, say, an array of all even numbers within a certain limits, or the nth member of the Fibonacci series. It seems unsound to have a number class be responsible for saving itself to a file, or retrieving information from the web.
For these and similar reasons, I believe the fetching and saving of Core Data entities belongs into controllers, not entity classes. Remember, one of the basic ideas behind the MVC (model-view-controller) pattern is that the controller manipulates the model or asks it for information, not that the model manipulates itself.
I speculate that your troubles are derived mainly from not separating the various functional aspects of your application sufficiently (data model, persistence, network operations, user interactions).
ugh... what I would do is make very naked NSManagedObject subclasses... then extend them with categories, that way when you regenerate your classes from the updated model you don't have to try to merge in all of your custom logic.
also the custom logic belongs in the model, the model contains the category or class extension.
so take that crap out of the View Controllers and put it in an easily maintainable category or several categories if it is warranted.