I have started to integrate Facebook in my app using StackMob.
I've been trying to use StackMob's offline sync, but it just seems broken.
In didFinishLaunchingWithOptions:
SM_CACHE_ENABLED = YES;
SMClient *client = [[SMClient alloc] initWithAPIVersion:#"0" publicKey:#"cc5f57c8-4a5d-424d-a3e1-0594c675cad8"];
SMCoreDataStore *coreDataStore = [client coreDataStoreWithManagedObjectModel:self.managedObjectModel];
coreDataStore.cachePolicy = SMCachePolicyTryCacheOnly;
_managedObjectContext = [coreDataStore contextForCurrentThread];
When a user logs in with Facebook:
[[SMClient defaultClient] loginWithFacebookToken:FBSession.activeSession.accessTokenData.accessToken createUserIfNeeded:YES usernameForCreate:nil onSuccess:^(NSDictionary *result) {
NSLog(#"Logged in with StackMob!");
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"User"];
[self.managedObjectContext executeFetchRequest:fetchRequest returnManagedObjectIDs:NO onSuccess:^(NSArray *results) {
NSLog(#"users: %#",results);
} onFailure:^(NSError *error) {
NSLog(#"error: %#",error);
}];
} onFailure:^(NSError *error) {
NSLog(#"Error: %#", error);
}];
The problem is that the fetchRequest's result is an empty array, although the user has been created successfully.
When I change my cache policy to SMCachePolicyTryCacheElseNetwork, the fetchRequest's result is not an empty array.
Why isn't the user saved in the cache?? Also, how can I tell StackMob to save some objects in the cache only?
Thank you so much!
It sounds like a bug you can report it here. You could work around this so you don't have to globally set SMCachePolicyTryCacheElseNetwork, by using a per request cache policy. Change your fetch request to:
SMRequestOptions *options = [SMRequestOptions options];
options.cachePolicy = SMCachePolicyTryCacheElseNetwork;
[self.managedObjectContext executeFetchRequest:fetchRequest
returnManagedObjectIDs:NO
successCallbackQueue:dispatch_get_main_queue()
failureCallbackQueue:dispatch_get_main_queue()
options:options
onSuccess:(SMResultsSuccessBlock)successBlock
onFailure:(SMFailureBlock)failureBlock];
Note that per request cache policies require the latest 2.1 SDK.
Related
I have a project that uses core data and this error happens intermittently. I know that the entity is there because most the time, the app opens and displays the content of the entityName.
1. this is happening in the app delegate and not being segue'd
2. when i do [self.managedObjectModel entities], the entityName is there but app crashes
3. It is not miss-spelled.
4. It occurs the same place, the same time (app start)
NSManagedObjectContext *contOBJ = self.managedObjectContext;
NSEntityDescription *entity;
NSString * entityForNameString = #"MessageLists";
#try {
entity = [NSEntityDescription entityForName:entityForNameString
inManagedObjectContext:contOBJ];
}
#catch (NSException* exception) {
NSLog(#"DANGER DANGER - ERROR FOUND");
NSLog(#"Uncaught exception: %#", exception.description);
// ditch effort to reset manageObject BUT DOES NOT WORK...
[self.managedObjectContext reset];
// ditch effort to reset manageObject BUT DOES NOT WORK...
return nil;
NSLog(#"Stack trace: %#", [exception callStackSymbols]);
// Reset the store
}
#finally {
NSLog(#"finally");
}
NSFetchRequest *fetcher = [[NSFetchRequest alloc] init];
// need to define a predicate that will institute weather a message thread is deleted or NOT
[fetcher setEntity:entity];
NSError *error;
NSLog(#"All Records is %#",[contOBJ executeFetchRequest:fetcher error:&error]);
return [contOBJ executeFetchRequest:fetcher error:&error];
Don't use that old stringly typed stuff, just do this
NSFetchRequest *fetchRequest = [MessageLists fetchRequest];
NSError *error;
NSLog(#"All Records is %#",[context executeFetchRequest:fetchRequest error:&error]);
The reason why my issue was intermittent is not because of my codes but rather changes to iOS 10. Apparently, apple made changes to the way core data is init and declared in the AppDelegate.
Starting in iOS 10 and macOS 10.12, the NSPersistentContainer handles
the creation of the Core Data stack and offers access to the
NSManagedObjectContext as well as a number of convenience methods.
Prior to iOS 10 and macOS 10.12, the creation of the Core Data stack
was more involved.
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/InitializingtheCoreDataStack.html
As it turns out, even if I implemented:
NSFetchRequest *fetchRequest = [MessageLists fetchRequest];
NSError *error;
NSLog(#"All Records is %#",[context executeFetchRequest:fetchRequest error:&error]);
I would still arrive at the same issue. Besides, I haven't updated my AppDelegate's Core Data codes since iOS 8...
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.
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!
I have method called collectData in my app which is the most important part of my View Controller. In that method I do a couple of signicant things (downloading, parsing, saving to persistent store), so it would be easier for you to take a look:
-(void)collectData
{
// Downloading all groups and saving them to Core Data
[[AFHTTPRequestOperationManager manager] GET:ALL_GROUPS parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSMutableDictionary* groups = [NSMutableDictionary new];
NSMutableArray* newIds = [NSMutableArray new];
NSError *error;
// Saving everything from response to MOC
for (id group in responseObject) {
Group *groupEntity = [NSEntityDescription insertNewObjectForEntityForName:#"Group" inManagedObjectContext:self.moc];
groupEntity.name = [group valueForKey:#"name"];
groupEntity.cashID = [group valueForKey:#"id"];
groupEntity.caseInsensitiveName = [[group valueForKey:#"name"] lowercaseString];
groupEntity.selected = #NO;
// Filling up helping variables
groups[groupEntity.cashID] = groupEntity;
[newIds addObject:groupEntity.cashID];
}
// Fetching existing groups from Persistant store
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:#"Group"];
[r setIncludesPendingChanges:NO];
r.predicate = [NSPredicate predicateWithFormat:#"cashID IN %#",newIds];
NSArray *existingGroups = [self.moc executeFetchRequest:r error:&error];
// Deleting groups which already are in database
for (Group* g in existingGroups) {
Group* newGroup = groups[g.cashID];
g.name = [newGroup valueForKey:#"name"];
g.cashID = [newGroup valueForKey:#"cashID"];
g.caseInsensitiveName = [[newGroup valueForKey:#"name"] lowercaseString];
[self.moc deleteObject:newGroup];
}
// Saving Entity modification date and setting it to pull to refresh
[self saveModificationDate:[NSDate date] forEntityNamed:#"Group"];
[self.pullToRefreshView.contentView setLastUpdatedAt:[self getModificationDateForEntityNamed:#"Group"]
withPullToRefreshView:self.pullToRefreshView];
// Save groups to presistant store
if (![self.moc save:&error]) {
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
[[self fetchedResultsController] performFetch:&error];
[self.pullToRefreshView finishLoading];
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Show alert with info about internet connection
[self.pullToRefreshView finishLoading];
UIAlertView *internetAlert = [[UIAlertView alloc] initWithTitle:#"Ups!" message:#"Wygląda na to, że nie masz połączenia z internetem" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[internetAlert show];
}];
}
So when I start collecting data (first run or push to refresh) this method is blocking UI.
I want to avoid this but when I put the success block into another dispatch_async and get back to main queue only for [self.tableView reloadData] I face problem with saving to persistent store or something with bad indexes.
How can I do this whole thing in background and leave UI responsive to the user?
Just an idea, give it a try using dispatch_sync. Have a look at this explanation here where log result something similar to your need. Put [yourTableView reloadData] after synchronous block.
Hope it helps!
It seems AFNetwork call is not async so just try to call your method via performselector.
I'm using core data to save some integer (rate) and then I call save in the context:
HeartRateBeat * beat = [HeartRateBeat heartRateWithHeartRate:rate
ofRecordTitle:self.recordTitle
inManagedObjectContext:document.managedObjectContext];
NSError * error;
[document.managedObjectContext save:&error];
Inside that convenient method I create the object using NSEntityDescription like this:
heartRateBeat = [NSEntityDescription insertNewObjectForEntityForName:#"HeartRateBeat" inManagedObjectContext:context];
(I only copied some important code, just to show what I did.)
I immediately execute a fetch request after every single heart beat inserted and managed object context saved (I save immediately), and the request shows that heart beat does appear to be stored inside Core Data (with growing array of heart beats), but if I restart my app (I'm using simulator BTW) I know things aren't actually getting saved to disk because it starts anew. Checking with SQLite3 command line shows empty tables. What am I missing here?
I get the same problem but I think its just because, I assume, like me you are just stopping the app through xcode and not actually closing it down. I use this code to force a write. Im using a UIManagedDocument, shared through appdelegate, rather than setting everything up manually.
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[[AppDelegate sharedAppDelegate].userDatabase saveToURL:[AppDelegate sharedAppDelegate].userDatabase.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
I don't know about you guys, but when I want my Core Data to save, my Core Data better save.
Here's some code that will for sure save all of your Core Datas.
-(void)forceSave {
NSManagedObjectContext * context = self.managedObjectContext; //Get your context here.
if (!context) {
NSLog(#"NO CONTEXT!");
return;
}
NSError * error;
BOOL success = [context save:&error];
if (error || !success) {
NSLog(#"success: %# - error: %#", success ? #"true" : #"false", error);
}
[context performSelectorOnMainThread:#selector(save:) withObject:nil waitUntilDone:YES];
[context performSelector:#selector(save:) withObject:nil afterDelay:1.0];
[context setStalenessInterval:6.0];
while (context) {
[context performBlock:^(){
NSError * error;
bool success = [context save:&error];
if (error || !success)
NSLog(#"success: %# - error: %#", success ? #"true" : #"false", error);
}];
context = context.parentContext;
}
NSLog(#"successful save!");
}
Note that this is BAD CODE. Among other problems, it's not thread-safe and takes too long. However, try using this and deleting some parts of it to your satisfaction :)