From what I've been reading about the largely undocumented NSAsynchronousFetchRequest, it is supposed to be cancelable. In Apple's video "What's New in Core Data" from WWDC 2014, there is an example of it being done (right around 17:40). But nowhere have I found how this is supposed to be done.
I've tried setting it up to cancel a fetch when a new fetch comes in, but I have been, seemingly, unsuccessful in getting this to work. The reason I say "seemingly" is because when I debug the code, it hits the cancel method of NSAsyncronousFetchResult's NSProgress property (and the property is not nil). However, after several previous fetches have been "cancelled" the app freezes for approximately the amount of time it would have taken to perform all the fetches. So, it doesn't seem like the fetches are being canceled. Here is what I am trying to cancel the fetch:
if (self.asyncFetchResult) {
[self.asyncFetchResult.progress cancel];
}
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"OfflineFeature"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"layers.layerName in %# AND xMax >= %lf AND xMin <= %lf AND yMax >= %lf AND yMin <=%lf", allLayerNames, bufferedEnvelope.xmin,bufferedEnvelope.xmax,bufferedEnvelope.ymin,bufferedEnvelope.ymax];
NSAsynchronousFetchRequest* asyncFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult* result) {
if (![result.progress isCancelled]) {
allFeatures = result.finalResult;
dispatch_async(dispatch_get_main_queue(), ^{
//Bunch of code to use the results
});
}
}];
MVAppDelegate* appDelegate = (MVAppDelegate*)[[UIApplication sharedApplication] delegate];
__weak typeof(self) weakSelf = self;
[appDelegate.managedObjectContext performBlock:^{
NSProgress* progress = [NSProgress progressWithTotalUnitCount:1];
[progress becomeCurrentWithPendingUnitCount:1];
NSError* error;
weakSelf.asyncFetchResult = [appDelegate.managedObjectContext executeRequest:asyncFetchRequest error:&error];
if (error) {
NSLog(#"Error performing asynchronous fetch request.\n%#", error);
}
[progress resignCurrent];
}];
I would appreciate any thoughts on what I'm doing wrong or if there's something else I could try that may be more appropriate. Thanks in advance.
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.
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 am working on iOS application using Core Data where I am fetching results from Core Data asynchronously. I need to figure out a way to implement a cancellation function of this fetch in case the user decides that they have been waiting too long, and wish to cancel the fetch part way through. I know this is possible using Core Data in iOS 8 using NSProgress, but I can't find any examples or sample code on how to do this.
Here is my sample fetch method:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MyObject"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"read == %#", #(NO)];
NSPersistentStoreAsynchronousFetchResultCompletionBlock resultBlock = ^(NSAsynchronousFetchResult *result) {
NSLog(#"Number of Unread Items: %ld", (long)result.finalResult.count);
[result.progress removeObserver:self
forKeyPath:#"completedUnitCount"
context:ProgressObserverContext];
[result.progress removeObserver:self
forKeyPath:#"totalUnitCount"
context:ProgressObserverContext];
};
NSAsynchronousFetchRequest *asyncFetch = [[NSAsynchronousFetchRequest alloc]
initWithFetchRequest:fetchRequest
completionBlock:resultBlock];
[context performBlock:^{
//Assumption here is that we know the total in advance and supply it to the NSProgress instance
NSProgress *progress = [NSProgress progressWithTotalUnitCount:preComputedCount];
[progress becomeCurrentWithPendingUnitCount:1];
NSAsynchronousFetchResult *result = (NSAsynchronousFetchResult *)[context
executeRequest:asyncFetch
error:nil];
[result.progress addObserver:self
forKeyPath:#"completedUnitCount"
options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew
context:ProgressObserverContext];
[result.progress addObserver:self
forKeyPath:#"totalUnitCount"
options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew
context:ProgressObserverContext];
[progress resignCurrent];
}];
I realize that by cancelling my fetch using NSProgress, I through an NSUserCancelled Error, but again, I would like to see an example of how this is done.
We're currently trying to get HealthKit to work in the background, in order to deliver steps data to our server when the App is closed.
For experimental purposes we've created a brand new iOS project in XCode, enabled HealhtKit and all background modes in Compabilities. After that, we pretty much run the code (see further down).
So what happens first is that the app ofcourse asks for the permissions, which we grant. What we're expecting is that the app should keep deliver the steps data every hour, to the server. But it doesnt do that, it seems like the app cant do anything when it's not active.
The app only deliver data when it gets resumed or started, but not at all from the background (Soft-closed / Hard-closed)
appdelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self setTypes];
return YES;
}
-(void) setTypes
{
self.healthStore = [[HKHealthStore alloc] init];
NSMutableSet* types = [[NSMutableSet alloc]init];
[types addObject:[HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount]];
[self.healthStore requestAuthorizationToShareTypes: types
readTypes: types
completion:^(BOOL success, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self observeQuantityType];
[self enableBackgroundDeliveryForQuantityType];
});
}];
}
-(void)enableBackgroundDeliveryForQuantityType{
[self.healthStore enableBackgroundDeliveryForType: [HKQuantityType quantityTypeForIdentifier: HKQuantityTypeIdentifierStepCount] frequency:HKUpdateFrequencyImmediate withCompletion:^(BOOL success, NSError *error) {
}];
}
-(void) observeQuantityType{
HKSampleType *quantityType = [HKSampleType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
HKObserverQuery *query =
[[HKObserverQuery alloc]
initWithSampleType:quantityType
predicate:nil
updateHandler:^(HKObserverQuery *query,
HKObserverQueryCompletionHandler completionHandler,
NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (completionHandler) completionHandler();
[self getQuantityResult];
});
}];
[self.healthStore executeQuery:query];
}
-(void) getQuantityResult{
NSInteger limit = 0;
NSPredicate* predicate = nil;
NSString *endKey = HKSampleSortIdentifierEndDate;
NSSortDescriptor *endDate = [NSSortDescriptor sortDescriptorWithKey: endKey ascending: NO];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType: [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount]
predicate: predicate
limit: limit
sortDescriptors: #[endDate]
resultsHandler:^(HKSampleQuery *query, NSArray* results, NSError *error){
dispatch_async(dispatch_get_main_queue(), ^{
// sends the data using HTTP
[self sendData: [self resultAsNumber:results]];
});
}];
[self.healthStore executeQuery:query];
}
I found this out a little while ago when talking to someone from Apple. Apparently you can't access HK data in the background if the device is locked:
NOTE
Because the HealthKit store is encrypted, your app cannot read data
from the store when the phone is locked. This means your app may not
be able to access the store when it is launched in the background.
However, apps can still write data to the store, even when the phone
is locked. The store temporarily caches the data and saves it to the
encrypted store as soon as the phone is unlocked.
from:
https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Framework/
I see something that might be causing an issue in your AppDelegate, particularly this line:
[[NSURLConnection alloc] initWithRequest:request delegate:self];
This is creating an NSURLConnection, but not starting it. Try changing it to this:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection start];
Edit: After taking a second look at the docs
They recommend setting up your observer queries in your application didFinishLaunchingWithOptions: method. In your code above, you set the HKObserverQuery up in the authorization handler, which is called on a random background queue. Try making this change to set it up on the main thread:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self setTypes];
[self observeQuantityType];
return YES;
}
HKObserverQuery Reference