I am building an app for displaying assets (PDF, Video, Etc).
It starts by downloading a JSON and parsing it into Core Data Objects <-- This part works fine.
These objects are a hierarchical set of Nodes that have a relationships set up in my model. each node can either be a FILE or a FOLDER. <-- no problems.
Then I have instance methods built into my NSManagedObject Subclasses that will download the file associated with that object (ie. a PDF). Then it sets
self.isAvailable = [NSNumber numberWithBool:YES];
Meanwhile, I have a UITableView that displays a list of assets. eventually it will update in real-time, but for now this is where I am having issue. I first had the view controller keep a pointer to the CoreData object that represents the folder it displays, but it appears that If the context gets updated, the pointer becomes invalid (ie. fails to fault).
Core data is not being very specific on what the problem is, or even where its happening, but it seems to crash when I set isAvailable with
*** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x1d5f9e50 <x-coredata://EDE66B97-B142-4E87-B445-76CAB965B676/Node/p58>''
I feel like the problem is that I shouldn't just keep a strong reference to a core data object as my model. Is there a better (less crashy) way to do this?
I have started playing with NSFetchedResultsController and using objectID's instead, but I haven't gotten anywhere yet.
- (void)populateChildren {
NSString * urlString = [NSString stringWithFormat:#"%#/%#", [CMPConstants hostURLString], self.SBUCode];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:self.downloadQueue completionHandler:^(NSURLResponse * response, NSData * data, NSError * error) {
if (data) {
NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
[self processParsedObject:dict];
} else {
NSLog(#"%#", urlString);
}
}];
}
#pragma mark - Parse JSON into NSManagedObjects
- (void)processParsedObject:(id)object {
[self processParsedObject:object depth:0 parent:nil key:nil];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
}
- (void)processParsedObject:(id)object depth:(int)depth parent:(Node *)parent key:(NSString*)key {
if ([object isKindOfClass:[NSDictionary class]]) {
if (depth == 0) {
// Grab content node if depth is 0;
object = [object valueForKey:#"content"];
}
// FIXME: Change this to a real primary key once we get one.
static NSString * primaryKey = #"name";
// Look for existing object
Node * testNode = [Node MR_findFirstByAttribute:primaryKey withValue:[object valueForKey:primaryKey]];
// Create new node pointer
Node * newNode;
if (testNode) {
// Update existing Node
newNode = testNode;
} else {
// Build a new Node Object
newNode = [Node MR_createEntity];
newNode.isAvailable = [NSNumber numberWithBool:NO];
}
// Get keys
NSArray * keys = #[#"name",
#"type",
#"index",
#"size",
#"videoDemensions",
#"videoId",
#"fileName",
#"fileType",
#"path"];
if ([[object valueForKey:#"type"] isEqual:[NSNull null]]) {
NSLog(#"%#", object);
}
// Loop to set value for keys.
for (NSString * key in keys) {
id value = [object valueForKey:key];
if (![[object valueForKey:key] isKindOfClass:[NSNull class]]) {
[newNode setValue:value forKey:key];
}
}
// Set calculated properties.
[newNode setSbu:[self SBUCode]];
[newNode setParent:parent];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
// Sync local file.
if (!newNode.isAvailable.boolValue) {
[newNode aquireFileInQueue:self.downloadQueue];
}
// Process children
for(NSString * newKey in [object allKeys]) {
id child = [object objectForKey:newKey];
[self processParsedObject:child depth:depth+1 parent:newNode key:newKey];
}
} else if ([object isKindOfClass:[NSArray class]]) {
for(id child in object) {
[self processParsedObject:child depth:depth+1 parent:parent key:nil];
}
} else {
// Nothing here, this processes each field.
}
}
This Method is an instance method of the Node class.
- (void)aquireFileInQueue:(NSOperationQueue *)queue {
if ([self.type isEqualToString:#"VIDEO"]) {
// Videos are available, but not downloaded.
self.isAvailableValue = YES;
return;
}
if (self.path == nil || self.fileName == nil) {
NSLog(#"Path or Filename for %# was nil", self.name);
return;
}
// Build the download URL !! MAKE SURE TO ADD PERCENT ESCAPES, this will protect against spaces in the file name
// Also make sure to slash-separate the path and fileName
NSURL * downloadURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#/%#",
[self.path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
[self.fileName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
// Build the download request
NSURLRequest * downloadRequest = [NSURLRequest requestWithURL:downloadURL];
// FIXME: Authentication Code for JSON service
// Show network activity indicator
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
// Send Asynchronus Request for fileData
[NSURLConnection sendAsynchronousRequest:(NSURLRequest *)downloadRequest queue:queue completionHandler:^(NSURLResponse * response, NSData * data, NSError * error) {
// Hide network activity indicatior
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
// Cast URL Response to HTTPURLResponse
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
// If statusCode is 200 (successful) and data is not nil, save data
if (httpResponse.statusCode == 200 && data) {
[data writeToURL:[self fileURL] atomically:NO];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self setIsAvailable:[NSNumber numberWithBool:YES]];
}];
}
}];
}
- (void)prepareForDeletion {
// Remove file from Filesystem
[[NSFileManager defaultManager] removeItemAtURL:[self fileURL] error:nil];
}
- (NSURL *)fileURL {
// Return local file URL
return [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/%#", [Node applicationDocumentsDirectory], self.fileName]];
}
I am not familiar with MagicalRecords
A 'Could not fullfil fault' error occur when a context is holding an un-faulted object (an object stub), but the actual object in the database does not exist (deleted or was never saved).
My first advice:
If you work in a multithreaded environment, try to hold faulted objects.
use -existingObjectWithId:error: and fetch requests with:
[fetchRequest setReturnsObjectsAsFaults:NO];
[fetchRequest setIncludesPropertyValues:YES];
[fetchRequest setRelationshipKeyPathsForPrefetching:/*relationships you can afford to prefetch*/];
My second advice (to debug your issue):
Print you deletedObjects set before each save you make to the store to see which context caused the fault.
My third advice:
merge changes to the main context (my guess is that MagicalRecords does that for you).
note 1: deletes may be implied (you don't explicitly use deleteObject: by setting a relationship in cascade/deny mode for example)
note 2: you can not avoid this exception in a multithreaded environment (AFAIK), unless you pass all your saves through the main context (using parentContext) or by always using prefetched objects (not using relationships directly).
Related
We are developing an application to collect beacon information and sync new/updated data to the server. We are using core data to collect beacon information, but occasionally we've found that the core data framework gives incorrect results when syncing to the server. Sometimes predicates are failing after the first iteration while fetching data from the core data to sync. So, we are seeking advice about any issues you have had with prolonged use of core data and persistent storage of the database.
I am using following steps to sync core data records to server one by one
-(void)syncDataToServer{
if( !isSycInProgress ){
//predicate fetch records which newly created and which are synced to server yet
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"isSynced == %#",[NSNumber numberWithBool:NO]];
//fetch only one record at one time to utilize memory
NSArray *beaconRecordArray = [[ISCoreDataManager sharedManager] fetchObjectList:#"BeaconRecords" predicate:predicate attributeName:#"endTime" batchSize:1 offset:0 isAscending:YES inContext:mainContext];
BeaconRecords *beacon = nil;
if(beaconRecordArray.count){
isSycInProgress = YES;
beacon = [beaconRecordArray firstObject];
{
//create request parameter dictionary
NSDictionary *dict = #{
#"uid": [beacon.uID description],
#"title":beacon.title.length?beacon.title:#"No title",
#"url": beacon.urlString?beacon.urlString:#"",
};
//call webservice to upload records on server
__block NSManagedObjectID *objId = beacon.objectID;
[[WebServiceHelper sharedInstance] callPostDataWithMethod:beconUrlPostAddBecon withParameters:dict withHud:YES success:^(NSDictionary *response)
{
if([response isKindOfClass:[NSDictionary class]] && [response[#"status"] intValue] == 200){
NSString *responseStr =response[#"response_crypt_data"];
NSError *error;
//Decrypt data to fetch parameters in response
NSData *decryptedData = [RNDecryptor decryptData:[[NSData alloc] initWithBase64EncodedString:responseStr options:0] withPassword:secretKey error:&error];
DLog(#"decrypted data %#", [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding]);
//parse data
//convert data into collection format (for parsing)
NSError *parseJsonError = nil;
NSDictionary *decryptedResponse = [NSJSONSerialization JSONObjectWithData:decryptedData options:NSJSONReadingMutableContainers error:&parseJsonError];
//store Server id in local db for future reference to update record
BeaconRecords *beaconRec = [self.privateContext existingObjectWithID:objId error:nil];
[self.privateContext performBlock:^{
if(!error && !parseJsonError){
beaconRec.serverID = [NSString stringWithFormat:#"%#",decryptedResponse[#"id"]];
beaconRec.isSynced = #(YES);
[[ISCoreDataManager sharedManager] dbSaveInContext:self.privateContext];
}
//update isSynced flag in local db
isSycInProgressBeaconRecords = NO;
dispatch_async( dispatch_get_main_queue(), ^{
[self syncDataToServer]; // call this method recursively to upload all records which are pending to sync
});
}];
}
else
{
isSycInProgress = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[self syncDataToServer]; // call this method recursively to upload all records which are pending to sync
});
}
} errorBlock:^(id error) {
isSycInProgress = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[self syncDataToServer]; // call this method recursively to upload all records which are pending to sync
});
}];
}
return;
}
isSycInProgress = NO;
}
}
After successfully syncing 1st record to server, when I call same sync function recursively to upload 2nd record, that predicate is giving me same record which I uploaded before. And strange thing is isSynched property of fetched object is (yes) and my predicate says it should be (_NO).
I've recently implemented the new AWS 2.0 iOS SDK in my application (yay, cocoapods!), and using the sample code from Amazon managed to properly configure access and downloads. I can successfully download a single item without issue, but I need to be able to download multiple files dynamically generated based on the current tableview. There doesn't appear to be a way to set up a batch download, so I'm simply trying to loop through an array of objects and trigger a download with each one. It works, but if the list includes more than a few items, it starts randomly misfiring. For example, if my dynamically created list has 14 items in it, 12 will be downloaded, and the other 2 aren't even attempted. The request just vanishes. In my testing, I added a sleep(1) timer, and then all 14 are triggered and downloaded, so I'm guessing that I'm overwhelming the download requests and they are getting dropped unless I slow it down. Slowing it down is not ideal... perhaps there is another way? Here is the code:
- (IBAction)downloadAllPics:(UIBarButtonItem *)sender {
if (debug==1) {
NSLog(#"Running %# '%#'", self.class, NSStringFromSelector(_cmd));
}
CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
// for loop iterates through all of the items in the tableview
for (Item *item in self.frc.fetchedObjects) {
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *downloadingFilePath1 = [NSString stringWithFormat:#"%#/%##2x.jpg",docDir, item.imageName];
NSURL *downloadingFileURL1 = [NSURL fileURLWithPath:downloadingFilePath1];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
if ([fileManager fileExistsAtPath:downloadingFilePath1]) {
fileAlreadyExists = TRUE;
if (![fileManager removeItemAtPath:downloadingFilePath1
error:&error]) {
NSLog(#"Error: %#", error);
}
}
__weak typeof(self) weakSelf = self;
self.downloadRequest1 = [AWSS3TransferManagerDownloadRequest new];
self.downloadRequest1.bucket = S3BucketName;
// self.downloadRequest1.key = S3KeyDownloadName1;
self.downloadRequest1.key = [NSString stringWithFormat:#"images/%##2x.jpg", item.imageName];
self.downloadRequest1.downloadingFileURL = downloadingFileURL1;
self.downloadRequest1.downloadProgress = ^(int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite){
// update progress
dispatch_sync(dispatch_get_main_queue(), ^{
weakSelf.file1AlreadyDownloaded = totalBytesWritten;
weakSelf.file1Size = totalBytesExpectedToWrite;
});
};
// this launches the actual S3 transfer manager - it is successfully launched with each pass of loop
[self downloadFiles];
}
[cdh backgroundSaveContext];
}
That launches the downloadFiles method:
- (void) downloadFiles {
//if I add this sleep, all 14 download. If I don't usually 11-13 download.
sleep(1);
AWSS3TransferManager *transferManager = [AWSS3TransferManager defaultS3TransferManager];
__block int downloadCount = 0;
[[transferManager download:self.downloadRequest1] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id(BFTask *task) {
if (task.error != nil){
if(task.error.code != AWSS3TransferManagerErrorCancelled && task.error.code != AWSS3TransferManagerErrorPaused){
NSLog(#"%s Errorx: [%#]",__PRETTY_FUNCTION__, task.error);
}
} else {
self.downloadRequest1 = nil;
}
return nil;
}];
}
There has got to be a way to download a dynamic list of files from an Amazon S3 bucket, right? Maybe there is a transfer manager that allows an array of files instead of doing them individually?
Any and all help is appreciated.
Zack
Sounds like request timeout interval setting issue.
First, when you configure AWSServiceConfiguration *configuration = ... try to configure the timeoutIntervalForRequest property. Also, maxRetryCount as well. maxRetryCount will attempt to download if failure for downloading each operation.
AWSServiceConfiguration *configuration = [[AWSServiceConfiguration alloc] initWithRegion:DefaultServiceRegionType
credentialsProvider:credentialsProvider];
[configuration setMaxRetryCount:2]; // 10 is the max
[configuration setTimeoutIntervalForRequest:120]; // 120 seconds
Second, for the multiple items downloading try to collect each AWSTask into one array and get the result at the end of group operation. ex)
// task collector
NSMutableSet *uniqueTasks = [NSMutableSet new];
// Loop
for (0 -> numOfDownloads) {
AWSS3TransferManagerDownloadRequest *downloadRequest = [AWSS3TransferManagerDownloadRequest new];
[downloadRequest setBucket:S3BucketNameForProductImage];
[downloadRequest setKey:filename];
[downloadRequest setDownloadingFileURL:sourceURL];
[showroomGroupDownloadRequests addObject:downloadRequest];
AWSTask *task = [[AWSS3TransferManager defaultS3TransferManager] download:downloadRequest];
[task continueWithBlock:^id(AWSTask *task) {
// handle each individual operation
if (task.error == nil) {
}
else if (task.error) {
}
// add to the tasks
[uniqueTasks addObject:task];
return nil;
}
[[AWSTask taskForCompletionOfAllTasks:tasks] continueWithBlock:^id(AWSTask *task) {
if (task.error == nil) {
// all downloads succeess
}
else if (task.error != nil) {
// failure happen one of download
}
return nil;
}];
The reason some requests seem to vanish is that you define AWSS3TransferManagerDownloadRequest as a property. self.downloadRequest1 = nil; is executed on the background thread, and it is possible that when [transferManager download:self.downloadRequest1] is executed, self.downloadRequest1 is nil.
You should remove the property and simply pass an instance of AWSS3TransferManagerDownloadRequest as an argument for - downloadFiles:.
I am currently developing an app that that will be used by association members that are going to a large annual conference.
The app will pull data from a database that is created by the app and populate it via a web service. The web service is split into 8 pages (this will likely go up). Each page represents a table in the database. The app will have several table views that will be populated by data in one or more of the tables in the database.
What I need is a the best method for going through the list of tables, connecting to their respective web service pages and then populating the respective database tables. This updating needs to take place in the background so the UI doesn't become unresponsive and/or show a downloading/updating/waiting kind of status.
So far I have a static array of the table names and have a loop that goes through the array and appends a URL string with the names, for example:
-(void)startUpdate
{
NSArray* tableNames = #[#"speaker", #"exhibitor", #"workshop", #"workshopspeakers", #"schedule", #"location", #"feedback", #"note", #"usage", #"user"];
NSUInteger loopCount = tableNames.count;
for (int i = 0; i < loopCount; ++i){
NSString *tableName = [tableNames objectAtIndex:i];
[self fetchObjectsWithTableName:[tableName mutableCopy] completion:^(NSArray* objects, NSError*error){
if (error) {
} else {
}
}];
}
}
fetchObjectsWithTableName method then has the connections and retrieves the data:
-(void)fetchData:(NSString *)tableName
withCompletion:(completion_t)completionHandler
{
NSString *currentURL = [NSString stringWithFormat:#"https://testapi.someURL.com/api/congress/%#", tableName];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:currentURL]];
[request addValue:#"application/json" forHTTPHeaderField:(#"Accept")];
[NSURLConnection sendAsynchronousRequest:request
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSError* err = error;
NSArray* objects; // final result array as a representation of JSON Array
if (response) {
NSHTTPURLResponse *newResp = (NSHTTPURLResponse*)response;
if (newResp.statusCode == 200) {
NSLog(#"FetchData - Status code = %li", (long)newResp.statusCode);
if ([data length] >0 && error == nil)
{
NSError* localError;
objects = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
if (objects) {
if (completionHandler) {
completionHandler(objects, nil);
}
//NSLog(#"Objects in current table - %# = %#", tableName, objects);
[self.tables addObject:objects];
// NSLog(#"Tables now = %#", self.tables);
NSLog(#"FetchData - Objects in current table - %# = %lu", tableName, (unsigned long)objects.count);
return;
} else {
err = localError;
}
} else {
NSLog(#"FetchData - objects is empty");
return;
// err = ...
}
}
NSLog(#"FetchData - Response code not 200#");
}
if (objects == nil) {
NSLog(#"FetchData - Nothing found in table: %#", tableName);
//assert(err);
if (completionHandler) {
completionHandler(nil, err);
}
}
}];
}
This currently goes through the array of table names, makes a connection based on each one and pulls back JSON data and stores it in a temporary array 'objects'. I think what I need now is that in each iteration of this 'objects' array is copied to the relevant table in the database, i.e. 'speaker' table name makes a connection: https://testapi.someURL.com/api/congress/speaker and the JSON is entered into the database under the table 'speaker'. How and where do I do that? Will I need to add a completion handler to startUpdate? If so, how? I don't understand completion handlers despite looking at several examples. Thanks.
No, do it in the NSURLConnection completion block after you have updated your temporary storage.
But, change your approach overall.
If you're only willing to change a bit, start using NSOperationQueue to limit the number of connections that you're trying to make at the same time. Preferably also use Core Data.
If you're willing to make a bigger change, definitely move to Core Data and look at using a framework like RestKit to do all of the download, mapping and storage for you.
(note, in both cases you need to set the max concurrent operation limit to prevent the app from flooding the network with requests - a limit of 5 should be good).
I need help understanding how to appropriate handle the following use case:
Say I'm writing a Chat app:
Launch App
Ask server (AFHTTPRequestOperation) to give me a list of all new messages
Loop through those messages to see if I need to download any images
If yes, then make another call to the server (AFImageRequestOperation) to get the image
I keep getting crashes where my managed object "message" is no longer in the same context, but I'm only using one managedObjectContext.
Is it because of the way I'm nesting the calls, since they are asynchronous? I am almost positive the message isn't getting deleted elsewhere, because I see it.
One thing to note is that is seems to happen when I change view controllers. I launch the app, and at the root view controller (RVC), it performs step #2 above. If I touch to go the the "MessageListViewController" (MLVC) before all of the images have downloaded, the associated messages for those images that didn't finish downloading suddenly have a nil managedObjectContext.
Below is the relevant code:
AFHTTPRequestOperation * operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:requestImageInfoURL
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSDictionary * JSONresponse = (NSDictionary *)JSON;
if( [[[JSONresponse objectForKey:#"status"] uppercaseString] isEqualToString:#"ERROR"] )
{
for( id<CommsObserver> observer in _observers )
[observer errorOccurred:[JSONresponse objectForKey:#"message"]];
}
else
{
if( [JSONresponse containsKey:#"convoMessages"] )
{
NSArray * messageList = [JSON objectForKey:#"messages"];
for( int i = 0 ; i < messageList.count ; i++ )
{
__block ConversationMessage * message = [JSONUtility convoMessageFromJSON:[messageList objectAtIndex:i]];
if( !message )
NSLog( #"Couldn't create the new message..." );
else
{
message.unread = [NSNumber numberWithBool:YES];
[[DataController sharedController] saveContext];
if( (!message.text || [message.text isEqualToString:#""]) && (message.image || message.imageInfoKey) )
{
NSString * imageURL = (message.image ? [Comms urlStringForImageInfo:message.image] : [Comms urlStringForImageKey:message.imageInfoKey extension:message.imageInfoExt]);
NSURLRequest *requestImageURL = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]];
AFImageRequestOperation * imageOperation;
imageOperation = [AFImageRequestOperation imageRequestOperationWithRequest:requestImageURL
imageProcessingBlock:^UIImage *(UIImage *image) {
return image;
} success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
if( message.image )
{
NSLog( #"updating imageInfo for message" );
[Utilities updateImageInfo:message.image
withImage:image
asPreview:YES
asThumbnail:YES
preserveSize:YES];
}
else
{
NSLog( #"creating a new imageInfo for message" );
ImageInfo * imageInfo = [Utilities createImageInfoFromImage:image asPreview:NO asThumbnail:NO preserveSize:YES];
if( imageInfo.managedObjectContext == nil )
NSLog( #"imageInfo MOC is NIL" );
else if( message.managedObjectContext == nil )
{
NSLog( #"message MOC is NIL" );
message = [[DataController sharedController] convoMessageForKey:message.key];
if( !message )
NSLog( #"message is NIL, meaning it wasn't found in the MOC" );
else if( !message.managedObjectContext )
NSLog( #"message MOC was STILL NIL" );
else
NSLog( #"problem solved..." );
}
if( imageInfo )
[[DataController sharedController] associateImageInfo:imageInfo withMessage:message];
}
[[DataController sharedController] saveContext];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog( #"Image DOWNLOAD error... \n%#" , [NSString stringWithFormat:#"%#" , error] );
}];
[imageOperation start];
}
for( id<CommsObserver> observer in _observers )
[observer newConvoMessages:#[message.key]];
}
} // End for loop of messageList
} // End if JSONresponse
} // End outer if ERROR statement
} // End Success
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog( #"Error: \n%#" , [NSString stringWithFormat:#"%#" , error] );
}
];
[operation start];
You need to ensure that the execution context where you invoke methods associated to a managed object context is appropriate (namely, is the same) as for the managed object context.
That is, when you invoke
[[DataController sharedController] saveContext];
the thread (or dispatch queue) where the method save: will be executed (eventually) MUST be the same where the managed object context is associated to.
Here in this case, we can immediately conclude, that this will only work IFF a) the completion handler of AFN will execute on the main thread AND b) the managed object context is associated to the main thread, too, OR you take care of this within the implementation of saveContext and use performBlock: or performBlockAndWait:.
Otherwise since the execution context of a managed object context is private, the execution context of any completion handler will never match this one. Hence, you violate the concurrency rules for Core Data.
Whenever you send a message to a managed object or a managed object context you need to ensure that the current execution context will be the correct one. That is, you need to use performBlock: or performBlockAndWait: and wrap accesses into the block:
[[DataController sharedController].managedObjectContext performBlock:^{
assert(message.managedObjectContext == [DataController sharedController].managedObjectContext);
message.unread = [NSNumber numberWithBool:YES];
[[DataController sharedController] saveContext];
...
}];
Note: you have to wrap all those accesses into either performBlock: or performBlockAndWait:, with the exception of property objectID of a managed object.
The objectID can be obtained from any thread. Thus this can be used to fetch any managed object into any context as long as you have the objectID.
A few other hints:
Using a Managed Object
You need to ensure that when you use a managed object (that is, send a message to it) that this will be executed on the same execution context which is associated to the managed object's managed object context.
That is, in order to ensure that, you use performBlock: or performBlockAndWait: as follows:
NSManagedObjectContext* context = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
Note: context uses a private queue.
__block NSManagedObject* obj;
[context performBlockAndWait:^{
obj = [context objectRegisteredForID:theObjectID];
}];
Assuming, the following statement will be executed on an arbitrary thread, this is unsafe:
NSString* name = obj.name;
"Unsafe", unless you know obj's managed object context has been associated to the main thread AND the above statement will also execute on the main thread. If the context uses a private queue, this will be never true unless you use performBlock: or performBlockAndWait::
Safe:
__block NSString* name;
[obj.managedObjectContext performBlockAndWait:^{
name = obj.name;
}];
Obtaining the objectID is always safe from any thread:
NSManagedObjectID* moid = obj.objectID; // safe from any thread
"Move" a managed object from one context to another:
You cannot use a managed object associated to context A, in context B. In order to "move" that object into context B you first need the objectID and then "fetch" this object in context B:
NSManagedObjectID* moid = obj.objectID
NSManagedObjectContext* otherContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[otherContext performBlock:^{
NSManagedObject* obj = [otherContext objectWithID: moid];
...
}];
Error Parameters
Be carefully with error parameters.
Error parameters are always autoreleased. performBlockAndWait: doesn't use an autorelease pool internally. So, you can have a __block variable error outside the block:
__block NSManagedObject* obj;
__block NSError* error;
[context performBlockAndWait:^{
obj = [context existingObjectWithID:theObjectID error:&error];
}];
if (obj==nil) {
NSLog(#"Error:%#", error);
}
However performBlock: will use an autorelease pool internally! This has consequences:
If you use the asynchronous version performBlock:, you need to handle errors within the block:
__block NSManagedObject* obj;
[context performBlock:^{
NSError* error;
obj = [context existingObjectWithID:theObjectID error:&error];
if (obj==nil) {
NSLog(#"Error:%#", error);
}
}];
I'm using GKPeerPickerController and GKSession classes and I'm trying to send rather large amount of data (appr 20 mb, images). The problem is that when I send more then, say 10 megabytes the appropriate delegate method of the receiver (- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context;) simply does not getting called. Is there some size restrictions? There is no completion handlers or errors returned. The data is sent to nowhere...I have another question also - is it possible to notify the sender that the data is received? (So that I can send queued packages). Thanks in advance!
Added
This method forms a dictionary with objects which I want to send
- (void)sendQuiz:(id<BlitzModelQuizProtocol>)quiz {
if ([quiz.backgroundType intValue] == BackgroundTypeUser) {
NSMutableDictionary * background = [NSMutableDictionary new];
NSString * filePath = [[BUIFileManager sharedInstance] filePathWithFileName:quiz.backgroundPath];
NSData * imageData = [NSData dataWithContentsOfFile:filePath];
[background setObject:imageData forKey:kQuizBackgroundImage];
[background setObject:quiz.backgroundPath forKey:kQuizBackgroundName];
[self.objectsToSend setObject:background forKey:kQuizBackground];
}
for (id<BlitzModelQuestionProtocol>question in quiz.questions) {
// Improve this logic when answers become > 1
if ([question.smileyType intValue] == SmileyTypeCustom) {
NSMutableArray * customSmiles = [NSMutableArray new];
for (id<BlitzModelAnswerProtocol>answer in question.answers) {
NSLog(#"smiley is: %#", answer.smiley);
NSMutableDictionary * smiles = [NSMutableDictionary new];
NSString * filePath = [[BUIFileManager sharedInstance] filePathWithFileName:answer.smiley];
NSData * imageData = [NSData dataWithContentsOfFile:filePath];
[smiles setObject:answer.smiley forKey:kSmileName];
[smiles setObject:imageData forKey:kSmileImage];
[customSmiles addObject:smiles];
}
[self.objectsToSend setObject:customSmiles forKey:kCustomSmiles];
}
}
NSArray * statistics = [self statisticsForQuizId:quiz.objectId];
if ([statistics count] > 0) {
NSMutableArray * blitzStatistics = [NSMutableArray new];
for (id<BlitzModelStatisticProtocol>stat in statistics) {
BlitzStatistic * statistic = [[BlitzStatistic alloc] initWithObject:stat];
[blitzStatistics addObject:statistic];
}
[self.objectsToSend setObject:blitzStatistics forKey:kStatiscticObjects];
}
else {
BlitzQuiz * quizModelObject = [[BlitzQuiz alloc] initWithObject:quiz];
[self.objectsToSend setObject:quizModelObject forKey:kQuizObject];
}
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:self.objectsToSend];
[self sendDataToPeers:data];
}
This is my sendData method:
- (void) sendDataToPeers:(NSData *) data {
NSString * title;
if (self.currentSession) {
NSError * error = nil;
if ([self.currentSession sendDataToAllPeers:data
withDataMode:GKSendDataReliable
error:&error]) {
NSLog(#"quiz sent");
}
else {
NSLog(#"error desc is: %#", [error localizedDescription]);
}
}
}
The method - (BOOL)sendDataToAllPeers:(NSData *)data withDataMode:(GKSendDataMode)mode error:(NSError **)error returns YES with no error (it's nil). What am I doing wrong?
Added
Sometimes the data is received successfully though sendData still returns NO without any error. Neither of delegate methods that handle error is getting called.