I have this code that is trying to do a background fetch for HealthKit data. The code works fine when I first run the app, but if I manually perform a background fetch (using the debug command), I get an exception thrown and an error that says reason: 'this request has been neutered - you can't call -sendResponse: twice nor after encoding it' and I'm not quite sure why.
Here is the code that fetches the data:
- (void)fetchNewDataWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
if (!self.healthStore) {
self.healthStore = [HKHealthStore new];
}
dataTypes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:1], HKQuantityTypeIdentifierStepCount,
[NSNumber numberWithInt:2], HKQuantityTypeIdentifierFlightsClimbed,
[NSNumber numberWithInt:3], HKQuantityTypeIdentifierDistanceWalkingRunning,
[NSNumber numberWithInt:4], HKQuantityTypeIdentifierDistanceCycling, nil];
achievementData = [NSMutableDictionary new];
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *startDate = [calendar startOfDayForDate:[NSDate date]];
NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];
for (NSString *key in dataTypes) {
HKSampleType *sampleType = [HKSampleType quantityTypeForIdentifier:key];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if (!error) {
if (!results) {
NSLog(#"No results were returned form query");
completionHandler(UIBackgroundFetchResultNoData);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self processNewDataWithResults:results andType:key];
completionHandler(UIBackgroundFetchResultNewData);
});
}
} else {
NSLog(#"Error: %# %#", error, [error userInfo]);
completionHandler(UIBackgroundFetchResultFailed);
}
}];
[self.healthStore executeQuery:query];
}
}
Then I have a separate function that processes the data that you can see gets called when results are found. If you want I can paste it in here but it is pretty lengthy, not sure if it has anything to do with it.
I have tried putting breakpoints in to see when the completion handler is called but from what I can tell it is only getting called once, unless I am missing something silly here.
If anyone has any advice, please let me know :) Thanks!
EDIT
Here is what the error message looks like:
2015-05-13 10:11:54.467 appName[379:169163] *** Assertion failure in -[UIFetchContentInBackgroundAction sendResponse:], /SourceCache/BaseBoard/BaseBoard-98.3/BaseBoard/BSAction.m:221
2015-05-13 10:11:54.470 appName[379:169163] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'this request has been neutered - you can't call -sendResponse: twice nor after encoding it'
*** First throw call stack:
(0x28ed4137 0x36e30c77 0x28ed400d 0x29bd2bc9 0x2dbde865 0x397ed5 0x2dbde7cf 0x2ca82a3d 0x39019b 0x390187 0x393e9d 0x28e99889 0x28e97fa9 0x28de39a1 0x28de37b3 0x305951a9 0x2c56e695 0xdff29 0x373d8aaf)
libc++abi.dylib: terminating with uncaught exception of type NSException
From looking at the code you posted, I do not see any other way than that the background fetch completion handler will be called exactly 4 times due to the for loop.
The code paths in the resultHandlers of each instance of HKSampleQuery will end up at a completionHandler(UIBackgroundFetchResult...) call in any case and you are always instantiating four of them, so this is what the assertion 'you can't call -sendResponse: twice' is complaining about IMO.
It should be easy to check if that is the problem by commenting out 3 of the queries from the dataTypes dictionary.
Edit: as requested in the comments, here's a possible solution (..just off the top of my head, so take it with a grain of salt..):
It (a) uses a semaphore lock to turn the asynchronous query result callback into a "synchronous" call & (b) uses a dispatch group to wait for all 4 queries to finish before executing the completion handler.
// NOTE: This example assumes that the fetch has "new data" if any of the queries returned something
// Also it skips the 'NSError' part (which could work exactly like the 'result' var)
// Define one or more block-global result variables for queries can put their end state into
UIBackgroundFetchResult __block result = UIBackgroundFetchResultNoData;
// Create a dispatch group that will manage all the concurrent queries
dispatch_queue_t queue = dispatch_queue_create([#"my.query.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t queries = dispatch_group_create();
// For each dataType dispatch a group block containing the query to run on the concurrent queue
for (NSString *key in dataTypes) {
dispatch_group_async(queries, queue, ^{
// To work around the asynchronous callback, I'll use a lock to wait for the query to return their result, so..
// ..like above, a block-global var will hold the result of the query
BOOL __block success = NO;
// ..create a one-time lock..
dispatch_semaphore_t lock = dispatch_semaphore_create(0);
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
success = YES; // ..or however you define success.. ;)
dispatch_semaphore_signal(lock); // ..open lock to signal result available..
}];
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); // ..wait for callback result lock to open..
// ..determine & set result.
if (success) {
result = UIBackgroundFetchResultNewData;
}
});
}
// Schedule a final block to execute (on the main queue) when all the other group blocks have finished running
dispatch_group_notify(queries, dispatch_get_main_queue(), ^{
// Determine final result and call completion handler
completionHandler(result);
});
Related
I am having an unexpected issue with a crash on my app which I am specifically struggling to debug because it occurs in background at a time determined by the iOS system. I have some capitalised comments to the code which show where the issue is being back traced to. I hope this is clear.
I believe it has to do with object deallocation.
I have tried using the __block before initialising the object but
this has not helped.
I have also tried dispatching the lines of code
in error to the main queue but that has not helped.
The actual crash is listed as AppName: __66-[BackgroundUpdateController initiateBackgroundHealthkitObservers]_block_invoke_5 + 160
I apologise if some of the code does not fit standard formatting and conventions. I am self taught from a variety of places and so do not have proper experience with code format.
Many Thanks
#import "BackgroundUpdateController.h"
NSUserDefaults *backgroundDefaults;
#implementation BackgroundUpdateController
-(id)init{
backgroundDefaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.HeartAnalyzer"];
return [super init];
}
-(void)initiateBackgroundHealthkitObservers{
// Check we should be running here
if(([backgroundDefaults integerForKey:#"sleepAnalysisEnabled"] != 1) || (![backgroundDefaults boolForKey:#"AutomaticSleepAdd"])) return;
// Initiate some variables, Use __block to ensure the backgroundHealthStore object does not get deallocated
__block HKHealthStore *backgroundHealthStore = [[HKHealthStore alloc] init];
HKQuantityType *activeEnergy = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierActiveEnergyBurned];
// Enable background delivery of active energy data from HealthKit
[backgroundHealthStore enableBackgroundDeliveryForType:activeEnergy frequency:HKUpdateFrequencyHourly withCompletion:^(BOOL success, NSError *error) {
}];
// Now setup an HKOberverQuery which triggers hourly if there are new active energy data points in HealthKit
HKObserverQuery *query = [[HKObserverQuery alloc] initWithSampleType:activeEnergy predicate:nil updateHandler:^(HKObserverQuery *query, HKObserverQueryCompletionHandler completionHandler, NSError *error) {
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
if (state == UIApplicationStateBackground || state == UIApplicationStateInactive){// Only run when app is not in foreground
// Load some more variables with checks to ensure they are valid objects
NSDate *previousSavedDate = [backgroundDefaults objectForKey:#"DateBackgroundSleepLastSaved"];
if(previousSavedDate == nil) previousSavedDate = [NSDate distantPast];
NSDate *lastSleepCheck = [backgroundDefaults objectForKey:#"LastSleepCheck"];
if(lastSleepCheck == nil) lastSleepCheck = [NSDate distantPast];
// If the last save date was long enough ago and the last sleep check was long enough ago, proceed
if(([previousSavedDate timeIntervalSinceNow] < -(3600*18)) && ([lastSleepCheck timeIntervalSinceNow] < -(3600*2))){
[backgroundDefaults setObject:[NSDate date] forKey:#"LastSleepCheck"];
[backgroundDefaults setBool:NO forKey:#"BackgroundSleepFound"];
SleepTimesCalculator *sleepClass = [[SleepTimesCalculator alloc] init];
[sleepClass calculateSleepTimes:^{
NSLog(#"Background sleep time calculations complete");
if([backgroundDefaults boolForKey:#"BackgroundSleepFound"]){// Only continue is a sleep time was found
__block NSMutableArray *savedSleepObjects = [backgroundDefaults valueForKey:#"SleepTimesDataBase"];
if(savedSleepObjects.count > 0){
__block NSMutableDictionary *sleepObject = [savedSleepObjects objectAtIndex:0]; // THE __BLOCK USED TO PREVENT THE OBJECT BEING DEALLOCATED, STILL SEEMS TO BE BASED ON THE CRASH
NSDate *sleepStart = [NSDate dateWithTimeIntervalSinceReferenceDate:[[sleepObject valueForKey:#"CalculatedSleepTime"]integerValue]];// Get the sleep time start date object
NSDate *sleepEnd = [NSDate dateWithTimeIntervalSinceReferenceDate:[[sleepObject valueForKey:#"CalculatedWakeTime"]integerValue]];
NSInteger sleepSavedToHealth = [[sleepObject valueForKey:#"SavedToHealth"] integerValue];// Check its not already been saved by some other element of the app
if(sleepSavedToHealth != 1){
HKCategorySample *sleepSample = [HKCategorySample categorySampleWithType:[HKCategoryType categoryTypeForIdentifier:HKCategoryTypeIdentifierSleepAnalysis] value:1 startDate:sleepStart endDate:sleepEnd];// Generate sleep object for HealthKit
[backgroundHealthStore saveObject:sleepSample withCompletion:^(BOOL success, NSError *error) {
if (!success) NSLog(#"Uncommon Error! saveObject:sleepSample");
else{
dispatch_async(dispatch_get_main_queue(), ^{// DISPATCH TO MAIN QUEUE AN ATTEMPTED FIX FOR CRASH
sleepObject = [savedSleepObjects objectAtIndex:0];// Choose the most recent sleep time to save
[sleepObject setValue:[NSNumber numberWithInteger:1] forKey:#"SavedToHealth"];// THIS IS WHERE THE 'Last Exception Backtrace (0)' ENDS UP
[savedSleepObjects replaceObjectAtIndex:0 withObject:sleepObject];// Replace the object which now has the 'Saved' tag
[backgroundDefaults setObject:[NSDate date] forKey:#"DateBackgroundSleepLastSaved"];// Save the data of the last time we reached this point
[backgroundDefaults setObject:savedSleepObjects forKey:#"SleepTimesDataBase"];// Save the sleep times back to the database
});
}
}];
}
completionHandler();// Call the completion handler as we've been throught the sleepObjects array
}
else completionHandler();// Call the completion handler anyway
}
else completionHandler();// Call the completion handler anyway
}];
}
else completionHandler();
}
}];
[backgroundHealthStore executeQuery:query];// Execute the HealthKit healthstore query
}
#end
Prefixing __block does not guarantees existence of an object for #"CalculatedSleepTime" key in sleepObject
I think you have misinterpreted how __block works. This will be a great guide.
On a quick overview of the code, it seems like [sleepObject valueForKey:#"CalculatedSleepTime"] is returning nil & without a nullability check you are trying to extract the integerValue
So, consider:
NSMutableDictionary *sleepObject = [savedSleepObjects objectAtIndex:0];
id calculatedSleepTime = [sleepObject valueForKey:#"CalculatedSleepTime"];
if(calculatedSleepTime){
NSDate *sleepStart = [NSDate dateWithTimeIntervalSinceReferenceDate:[calculatedSleepTime integerValue]];
}
And it looks like you also don't require the __block prefix in HKHealthStore *backgroundHealthStore = [[HKHealthStore alloc] init];
I am trying to record a 3 digit NSNumber to health kit as a PeakExpiratoryFlowRate, the code is below
- (void)PeakFlowupdate:(NSNumber *)pkFlow
{
NSString *identifier = HKQuantityTypeIdentifierPeakExpiratoryFlowRate;
HKQuantityType *peakFlowType = [HKObjectType quantityTypeForIdentifier:identifier];
HKQuantity *myPeakFlow = [HKQuantity quantityWithUnit:[HKUnit minuteUnit] doubleValue:[pkFlow doubleValue]];
HKQuantitySample *peakFlowSample = [HKQuantitySample quantitySampleWithType:peakFlowType quantity:myPeakFlow startDate:[NSDate date] endDate:[NSDate date]];
[healthStore saveObject:peakFlowSample withCompletion:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"SAVED!");
} else {
NSLog(#"%#", error);
}
}];
}
However I am receiving this error when I try to save the data to health kit
Terminating app due to uncaught exception
'_HKObjectValidationFailureException', reason: 'HKQuantitySample 250
min 2016-06-22 10:32:24 +1200 2016-06-22 10:32:24 +1200 requires unit
of type Volume/Time. Incompatible unit: min
As the message of the exception states, you cannot instantiate a peak flow sample with a quantity specified in minutes. Peak flow is measured in units of Volume/Time, so you'll need to use something like [[HKUnit literUnit] unitDividedByUnit:[HKUnit minuteUnit]].
The purpose is trigger a method when the user walks the required steps.
here is my code:
if ([HKHealthStore isHealthDataAvailable]) {
self.healthStore = [[HKHealthStore alloc] init];
NSSet *stepsType =[NSSet setWithObject:[HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount]];
[self.healthStore requestAuthorizationToShareTypes:nil readTypes:stepsType completion:^(BOOL success, NSError * _Nullable error) {
if (success) {
__block double stepsCount = 0.0;
HKSampleType *sampleType = [HKSampleType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
HKSampleQuery *sampleQuery = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:nil limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if (!error && results>0) {
for (HKQuantitySample *result in results) {
stepsCount += [result.quantity doubleValueForUnit:[HKUnit countUnit]];
}
}
}];
[self.healthStore executeQuery:sampleQuery];
double currentSteps = stepsCount;
while (1) {
[self.healthStore stopQuery:sampleQuery];
[self.healthStore executeQuery:sampleQuery];
if (currentSteps + requiredSteps >= stepsCount) {
[self triggerOneMethod];
break;
}
}
}
}];
}
But when I run the app, Xcode shows:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: 'You cannot start a query that is already active'
***
I have read the HealthKit Document, it says that
HealthKit executes queries asynchronously on a background queue. Most
queries automatically stop after they have finished executing.
and stopQuery: is to stop a long-running query.
I think these two points are what really matter.
Is it possible to achieve the purpose? If so, how can I fix it?
In the loop, you must create a new HKSampleQuery before calling executeQuery:. You cannot reuse an HKQuery instance.
I am working on my first iPhone App: a simple app showing the heartRate results from HealthKit in a nice way. My first step is to show the results as a raw text. But unfortunately I'm getting an exception at the following line, telling me: "thread 1 signal SIGABRT". Does someone know, what I did wrong and hint me in a direction?
double usersBeatsPerMinute = [quantity doubleValueForUnit:[HKUnit countUnit]];
The rest of the code looks like this:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// Set up an HKHealthStore, asking the user for read/write permissions. The profile view controller is the
// first view controller that's shown to the user, so we'll ask for all of the desired HealthKit permissions now.
// In your own app, you should consider requesting permissions the first time a user wants to interact with
// HealthKit data.
if ([HKHealthStore isHealthDataAvailable]) {
NSSet *writeDataTypes = [self dataTypesToWrite];
NSSet *readDataTypes = [self dataTypesToRead];
[self.healthStore requestAuthorizationToShareTypes:writeDataTypes readTypes:readDataTypes completion:^(BOOL success, NSError *error) {
if (!success) {
NSLog(#"You didn't allow HealthKit to access these read/write data types. In your app, try to handle this error gracefully when a user decides not to provide access. The error was: %#. If you're using a simulator, try it on a device.", error);
return;
}
}];
}
HKQuantityType *weightType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeartRate];
// Since we are interested in retrieving the user's latest sample
// we sort the samples in descending order by end date
// and set the limit to 1
// We are not filtering the data, and so the predicate is set to nil.
NSSortDescriptor *timeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:HKSampleSortIdentifierEndDate ascending:NO];
// construct the query & since we are not filtering the data the predicate is set to nil
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:weightType predicate:nil limit:1 sortDescriptors:#[timeSortDescriptor] resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
// if there is a data point, dispatch to the main queue
if (results) {
dispatch_async(dispatch_get_main_queue(), ^{
HKQuantitySample *quantitySample = results.firstObject;
// pull out the quantity from the sample
HKQuantity *quantity = quantitySample.quantity;
double usersBeatsPerMinute = [quantity doubleValueForUnit:[HKUnit countUnit]];
_HeartRateResults.text = [NSString stringWithFormat:#"%# lbs", [NSNumberFormatter localizedStringFromNumber:#(usersBeatsPerMinute) numberStyle:NSNumberFormatterNoStyle]];
});
}
}];
// do not forget to execute the query after its constructed
[_healthStore executeQuery:query];}
There was a comment in the documentation ("These samples use count/time units") I didn't quite understand, so I did a little searching and tried it out and was able to get a value I manually put into the Health app using this:
double rate = [mostRecentQuantity doubleValueForUnit:[[HKUnit countUnit] unitDividedByUnit:[HKUnit minuteUnit]]];
I haven't seen unitDividedByUnit before. Here's the article I pulled it from.
I'm experiencing strange CoreData issue.
First of all, in my project i use lot of frameworks, so there are many sources of problem - so i considered to create minimal project which repeats my issue. You can clone Test project on Github and repeat my test step-by-step.
So, the problem:
NSManagedObject is tied to it's NSManagedObjectID which doesn't let object to be deleted from NSManagedObjectContext properly
So, steps to reproduce:
In my AppDelegate, i setup CoreData stack as usual. AppDelegate has managedObjectContext property, which can be accessed to obtain NSManagedObjectContext for main thread. Application's object graph consists of one entity Message with body, from, timestamp attributes.
Application has only one viewController with only method viewDidLoad. It looks so:
- (void)viewDidLoad
{
[super viewDidLoad];
NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSEntityDescription *messageEntity = [NSEntityDescription entityForName:NSStringFromClass([Message class]) inManagedObjectContext:context];
// Here we create message object and fill it
Message *message = [[Message alloc] initWithEntity:messageEntity insertIntoManagedObjectContext:context];
message.body = #"Hello world!";
message.from = #"Petro Korienev";
NSDate *now = [NSDate date];
message.timestamp = now;
// Now imagine that we send message to some server. Server processes it, and sends back new timestamp which we should assign to message object.
// Because working with managed objects asynchronously is not safe, we save context, than we get it's objectId and refetch object in completion block
NSError *error;
[context save:&error];
if (error)
{
NSLog(#"Error saving");
return;
}
NSManagedObjectID *objectId = message.objectID;
// Now simulate server delay
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
// Refetch object
NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
Message *message = (Message*)[context objectWithID:objectId]; // here i suppose message to be nil because object is already deleted from context and context is already saved.
message.timestamp = [NSDate date]; // However, message is not nil. It's valid object with data fault. App crashes here with "Could not fulfill a fault"
NSError *error;
[context save:&error];
if (error)
{
NSLog(#"Error updating");
return;
}
});
// Accidentaly user deletes message before response from server is returned
delayInSeconds = 2.0;
popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
// Fetch desired managed object
NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"timestamp == %#", now];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Message class])];
request.predicate = predicate;
NSError *error;
NSArray *results = [context executeFetchRequest:request error:&error];
if (error)
{
NSLog(#"Error fetching");
return;
}
Message *message = [results lastObject];
[context deleteObject:message];
[context save:&error];
if (error)
{
NSLog(#"Error deleting");
return;
}
});
}
Well, i detected app crash so i try to fetch message another way. I changed fetch code:
...
// Now simulate server delay
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
// Refetch object
NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"timestamp == %#", now];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Message class])];
request.predicate = predicate;
NSError *error;
NSArray *results = [context executeFetchRequest:request error:&error];
if (error)
{
NSLog(#"Error fetching in update");
return;
}
Message *message = [results lastObject];
NSLog(#"message %#", message);
message.timestamp = [NSDate date];
[context save:&error];
if (error)
{
NSLog(#"Error updating");
return;
}
});
...
Which NSLog'ed message (null)
So, it shows:
1) Message is actually not existent in DB. It cannot be fetched.
2) First version of code someway kept deleted message object in context (Probably cause it's object id was retained for block call).
But why i could obtain deleted object by its id? I need to know.
Obviously, first of all, i changed objectId to __weak. Got crash even before blocks:)
So CoreData is built without ARC? Hmm interesting.
Well, i considered to copy NSManagedObjectID. What i've gotten?
(lldb) po objectId
0xc28ed20 <x-coredata://8921D8F8-436C-4CBC-B4AB-118198988D88/Message/p4>
(lldb) po message.objectID
0xc28ed20 <x-coredata://8921D8F8-436C-4CBC-B4AB-118198988D88/Message/p4>
See what's wrong? NSCopying's -copy is implemented like return self on NSManagedObjectID
Last try was __unsafe_unretained for objectId. Here we go:
...
__unsafe_unretained NSManagedObjectID *objectId = message.objectID;
Class objectIdClass = [objectId class];
// Now simulate server delay
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
if (![NSObject safeObject:objectId isMemberOfClass:objectIdClass])
{
NSLog(#"Object for update already deleted");
return;
}
...
safeObject:isMemberOfClass: implementation:
#ifndef __has_feature
#define __has_feature(x) 0
#endif
#if __has_feature(objc_arc)
#error ARC must be disabled for this file! use -fno-objc-arc flag for compile this source
#endif
#import "NSObject+SafePointer.h"
#implementation NSObject (SafePointer)
+ (BOOL)safeObject:(id)object isMemberOfClass:(__unsafe_unretained Class)aClass
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-objc-isa-usage"
return ((NSUInteger*)object->isa == (NSUInteger*)aClass);
#pragma clang diagnostic pop
}
#end
Brief explanation - we use __unsafe_unretained variable, so at time of block call it can be freed, so we have to check whether it's valid object. So we save it's class before block (it's not retain, it's assign) and check it in block via safePointer:isMemberOfClass:
So for now, refetching object by it's managedObjectId is UNTRUSTED pattern for me.
Does anybody have any suggestions how i should do in this situation? To use __unsafe_unretained and check? However, this managedObjectId can be also retained by another code, so it will cause could not fulfill crash on property access. Or to fetch object everytime by predicate? (and what to do if object is uniquely defined by 3-4 attributes? Retain them all for completion block?). What is the best pattern for working with managed objects asynchronously?
Sorry for long research, thanks in advance.
P.S. You still can repeat my steps or make your own experiments with Test project
Don't use objectWithID:. Use existingObjectWithID:error:. Per the documentation, the former:
... always returns an object. The data in the persistent store
represented by objectID is assumed to exist—if it does not, the
returned object throws an exception when you access any property (that
is, when the fault is fired). The benefit of this behavior is that it
allows you to create and use faults, then create the underlying data
later or in a separate context.
Which is exactly what you're seeing. You get an object back because Core Data thinks you must want one with that ID even though it doesn't have one. When you try to store to it, without having created an actual object in the interim, it doesn't know what to do and you get the exception.
existingObject... will return an object only if one exists.