Mac OS App crashes when saving too many new NSManagedObjects - objective-c

I'm trying to write an app that enumerates through folders and items which are dragged onto the main app window and then places a new entry for each PDF it finds into my Core Data database which in turn populates an NSTableView. To cut a long story short[er], I've got to a stage where I can safely drag a few items into the window and the database gets saved, allowing me to restart the app time after time.
The problem I have is that if I were to [say] drag my Documents folder onto the window it all appears to work but when I quit and attempt to restart then it crashes out with a
Thread 1: EXC_BAD_ACCESS (code=2, address-0x7fff6f11dff8
The code I'm currently using (specifically written for debugging this matter) is:
NSError *error;
id firstObject = [draggedStuff objectAtIndex:0];
NSDictionary *fileAttribs = [[NSFileManager defaultManager] attributesOfItemAtPath:firstObject error:&error];
if ([[fileAttribs valueForKey:NSFileType] isEqualTo:NSFileTypeDirectory]) {
NSManagedObject *aNewFolder = [[NSManagedObject alloc]initWithEntity:[NSEntityDescription entityForName:kFOLDERS_ENTITY inManagedObjectContext:[self managedObjectContext]] insertIntoManagedObjectContext:[self managedObjectContext]];
[aNewFolder setValue:firstObject forKey:kFOLDER_PATH];
NSArray *directoryContents = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:firstObject error:&error];
assert(!error);
[directoryContents enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
CFStringRef fileExtension = (__bridge CFStringRef) [obj pathExtension];
CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, NULL);
if (UTTypeConformsTo(fileUTI, kUTTypePDF)) {
NSManagedObject *aDocument = [[NSManagedObject alloc]initWithEntity [NSEntityDescription entityForName:kDOCO_ENTITY inManagedObjectContext:[self managedObjectContext]] insertIntoManagedObjectContext:[self managedObjectContext]];
[aNewPicture setValue:obj forKey:#"docoPath"];
}
CFRelease(fileUTI);
if (idx % 20 == 0) {
NSError *error;
[[self managedObjectContext] save:&error];
assert(!error);
}
}];
}
Initially, I had this in an NSOperation subclass with the relevent checking for user cancellation but I've moved it to the main thread just to make sure that it's not something to do with that, so, yes, it is currently not a user-friendly piece of code.
The crashing is definitely connected to the number of items that directoryContents returns with as it works absolutely fine if I drag a folder with only a matter of 20 or so items inside it. If directoryContents holds a matter of around 200 files or more then Core Data doesn't seem to save it correctly and corrupts the storedata file which needs to be Trashed before I can restart the app.
The if (idx % 20 == 0)... can be changed to save with more or less NSManagedObjects waiting to be saved but with the same results as well.
I've also tried using an NSDirectoryEnumerator with the same results: the corruption is always connected with the number of items in the folder.
I like Core Data, but sometimes I lose my way so any help is much appreciated especially given the length of this post!
Todd.

Turns out that it was much simpler than I thought: I'd managed to wire up my NSTable incorrectly.
I'd connected both the Table View and the Table column in the NIB to the entity.... Oops.

Related

Core Data save and Concurrency problems in nested loops + CloudKit

I'm using CloudKit to download an array of records (contained in myArray) The myArray enumeration is within the completion handler of the CloudKit block. There are a few nested CloudKit queries and array enumerations (example below). From there, I'm creating managed objects in a loop, and saving them, which will run only on first launch and then I'd expect Core Data to have them available persistently, so the app is designed to retrieve them without the need of re-creating them.
The problem is that my objects do not appear to save, as on the apps second launch the views are empty (or that the app saves some, or it crashes), and will only fill if I re-run the code to create the objects.
I think the issue may to do with concurrency issues / threads + Core Data - which seems to be the case after adding the compiler flag suggested. Consequently, I edited my code to make use of private queues for the NSManagedObjectContext, but still have crashes. I've edited the example below to show what my code looks like now.
I've simplified the code below for clarify and the purpose of the example, but it is more or less what I have:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//Download records from CloudKit, the following enumeration is within the CloudKit completion handler
[myArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
MyManagedObj *managedObjToInsert = [NSEntityDescription
insertNewObjectForEntityForName:#"entityName"
inManagedObjectContext:self.managedObjectContext];
managedObjToInsert.property = obj[#"property"];
//Get some new records from CloudKit with predicate based on this object which is related to the new records, this next block enumeration is in the completion handler of the new query
[myNextArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
MyManagedObj *nextManagedObjToInsert = [NSEntityDescription
insertNewObjectForEntityForName:#"entityName"
inManagedObjectContext:self.managedObjectContext];
nextManagedObjToInsert.property = obj[#"property"];
nextManagedObjToInsert.relatedObj = managedObjToInsert; //relational
}];
}];
NSError *error;
if (![self.managedObjectContext save:&error])
{
NSLog(#"Problem saving: %#", [error localizedDescription]);
}
}
I've added the flag suggested in the answers below, and it seems like my managedobjectcontext is being passed outside the main thread, giving unpredictable results. Where do or how do I use the private queue blocks - assuming that is the solution to the problem?
Turn on concurrency debugging if you are worried about the threads. Add this as a command line parameter to the start up
-com.apple.CoreData.ConcurrencyDebug 1
see an example here; http://oleb.net/blog/2014/06/core-data-concurrency-debugging/
I think you are correct in worrying about threads because you can't be sure what thread your CloudKit completion block is being run. Try wrapping your object creation loop (and the save) within a [self.managedObjectContext performBlockAndWait] section.
Solved this issue using private queues with the help of the following documentation (in addition to the helpful comments/answers shared before this answer):
Correct implementation of parent/child NSManagedObjectContext
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Concurrency.html
http://benedictcohen.co.uk/blog/archives/308
The problem was that I was trying to save to the NSManagedObjectContext on the main thread whilst the code being executed by the cloudkit query to the database was occuring on another thread, resulting in crashes and inconsistent saves to the persistent store. The solution was to use the NSPrivateQueueConcurrencyType by declaring a new child context (this is only supported in iOS 5+).
Working code now looks like*:
//create a child context
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[privateContext setParentContext:[self managedObjectContext]];
//Download records from CloudKit by performing a query
[publicDatabase performQuery:myQuery inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * resultsArray, NSError * error) {
[resultsArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
__block NSManagedObjectID *myObjID;
//Async and not in main thread so requires a private queue
[privateContext performBlockAndWait:^{
MyManagedObj *managedObjToInsert = [NSEntityDescription insertNewObjectForEntityForName:#"entityOne" inManagedObjectContext:privateContext];
managedObjToInsert.property = obj[#"property"];
myObjID = managedObjToInsert.objectID;
NSError *error;
if (![privateContext save:&error]) //propergates to the parent context
{
NSLog(#"Problem saving: %#", [error localizedDescription]);
}
}];
//Get some new records from CloudKit with predicate based on this object which is related to the new records, this next block enumeration is in the completion handler of the new query
[publicDatabase performQuery:mySecondQuery inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * secondResultsArray, NSError * error) {
[secondResultsArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
[privateContext performBlockAndWait:^{
MyManagedObj *nextManagedObjToInsert = [NSEntityDescription insertNewObjectForEntityForName:#"entityTwo" inManagedObjectContext:privateContext];
nextManagedObjToInsert.property = obj[#"property"];
NSManagedObject *relatedObject = [privateContext objectWithID:myObjID];
nextManagedObjToInsert.relatedObj = relatedObject; //relational
}];
}];
}];
NSError *childError = nil;
if ([privateContext save:&childError]) { //propagates to the parent context
[self.managedObjectContext performBlock:^{
NSError *parentError = nil;
if (![self.managedObjectContext save:&parentError]) { //saves to the persistent store
NSLog(#"Error saving parent %#", parentError);
}
}];
} else {
NSLog(#"Error saving child %#", childError);
}
}];
}];
*please note that this is just an example to show how I solved the problem - there may be certain variable declarations missing, but the gist of the solution is there.

Save multiple images quickly in iOS 6 (custom album)

I'm writing an application that will take several images from URL's, turn them into a UIImage and then add them to the photo library and then to the custom album. I don't believe its possible to add them to a custom album without having them in the Camera Roll, so I'm accepting it as impossible (but it would be ideal if this is possible).
My problem is that I'm using the code from this site and it does work, but once it's dealing with larger photos it returns a few as 'Write Busy'. I have successfully got them all to save if I copy the function inside its own completion code and then again inside the next one and so on until 6 (the most I saw it take was 3-4 but I don't know the size of the images and I could get some really big ones) - this has lead to the problem that they weren't all included in the custom album as they error'd at this stage too and there was no block in place to get it to repeat.
I understand that the actual image saving is moved to a background thread (although I don't specifically set this) as my code returns as all done before errors start appearing, but ideally I need to queue up images to be saved on a single background thread so they happen synchronously but do not freeze the UI.
My code looks like this:
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:singleImage]]];
[self.library saveImage:image toAlbum:#"Test Album" withCompletionBlock:^(NSError *error) {
if (error!=nil) {
NSLog(#"Error");
}
}];
I've removed the repetition of the code otherwise the code sample would be very long! It was previously where the NSLog code existed.
For my test sample I am dealing with 25 images, but this could easily be 200 or so, and could be very high resolution, so I need something that's able to reliably do this over and over again without missing several images.
thanks
Rob
I've managed to make it work by stripping out the save image code and moving it into its own function which calls itself recursively on an array on objects, if it fails it re-parses the same image back into the function until it works successfully and will display 'Done' when complete. Because I'm using the completedBlock: from the function to complete the loop, its only running one file save per run.
This is the code I used recursively:
- (void)saveImage {
if(self.thisImage)
{
[self.library saveImage:self.thisImage toAlbum:#"Test Album" withCompletionBlock:^(NSError *error) {
if (error!=nil) {
[self saveImage];
}
else
{
[self.imageData removeObject:self.singleImageData];
NSLog(#"Success!");
self.singleImageData = [self.imageData lastObject];
self.thisImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.singleImageData]]];
[self saveImage];
}
}];
}
else
{
self.singleImageData = nil;
self.thisImage = nil;
self.imageData = nil;
self.images = nil;
NSLog(#"Done!");
}
}
To set this up, I originally used an array of UIImages's but this used a lot of memory and was very slow (I was testing up to 400 photos). I found a much better way to do it was to store an NSMutableArray of URL's as NSString's and then perform the NSData GET within the function.
The following code is what sets up the NSMutableArray with data and then calls the function. It also sets the first UIImage into memory and stores it under self.thisImage:
NSEnumerator *e = [allDataArray objectEnumerator];
NSDictionary *object;
while (object = [e nextObject]) {
NSArray *imagesArray = [object objectForKey:#"images"];
NSString *singleImage = [[imagesArray objectAtIndex:0] objectForKey:#"source"];
[self.imageData addObject:singleImage];
}
self.singleImageData = [self.imageData lastObject];
self.thisImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.singleImageData]]];
[self saveImage];
This means the rest of the getters for UIImage can be contained in the function and the single instance of UIImage can be monitored. I also log the raw URL into self.singleImageData so that I can remove the correct elements from the array to stop duplication.
These are the variables I used:
self.images = [[NSMutableArray alloc] init];
self.thisImage = [[UIImage alloc] init];
self.imageData = [[NSMutableArray alloc] init];
self.singleImageData = [[NSString alloc] init];
This answer should work for anyone using http://www.touch-code-magazine.com/ios5-saving-photos-in-custom-photo-album-category-for-download/ for iOS 6 (tested on iOS 6.1) and should result in all pictures being saved correctly and without errors.
If saveImage:toAlbum:withCompletionBlock it's using dispatch_async i fear that for i/o operations too many threads are spawned: each write task you trigger is blocked by the previous one (bacause is still doing I/O on the same queue), so gcd will create a new thread (usually dispatch_async on the global_queue is optimized by gcd by using an optimized number of threads).
You should either use semaphores to limit the write operation to a fixed number at the same time or use dispatch_io_ functions that are available from iOS 5 if i'm not mistaken.
There are plenty example on how to do this with both methods.
some on the fly code for giving an idea:
dispatch_semaphore_t aSemaphore = dispatch_semaphore_create(4);
dispatch_queue_t ioQueue = dispatch_queue_create("com.customqueue", NULL);
// dispatch the following block to the ioQueue
// ( for loop with all images )
dispatch_semaphore_wait(aSemaphore , DISPATCH_TIME_FOREVER);
[self.library saveImage:image
toAlbum:#"Test Album"
withCompletionBlock:^(NSError *error){
dispatch_semaphore_signal(aSemaphore);
}];
so every time you will have maximum 4 saveImage:toAlbum, as soon as one completes another one will start.
you have to create a custom queue, like above (the ioQueue) where to dispatch the code that does the for loop on the images, so when the semaphore is waiting the main thread is not blocked.

iCloud NSMetadata query results are blank

I've been working on adding icloud to my project (which is quite a pain in the buns) and I'm able to save and remove files, but I can't get a list of the files stored in iCloud. I've tried solutions from about 10 different websites (including the Apple documentation). Whenever I call [self.query startQuery]; everything seems to be working: The correct methods get called, the methods execute exactly as they should. Then when I ask for an nsarray of the files in my app's iCloud Documents directory I get two parenthesis with nothing between them (when I view in NSLog): File List: ( ). I know for a fact that there are many different documents of all shapes, extensions, sizes, and names in my app's iCloud Documents directory because I've been using the iCloud Developer site to check if things are working. My first method to setup the query is as follows:
- (void)syncWithCloud {
self.query = [[NSMetadataQuery alloc] init];
NSURL *mobileDocumentsDirectoryURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
[query setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope, nil]];
//[query setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE '*'", NSMetadataItemFSNameKey]];
[query setPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:#"%%K like \"%#*\"", [mobileDocumentsDirectoryURL path]], NSMetadataItemPathKey]];
//Pull a list of all the Documents in The Cloud
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(processFiles:)
name:NSMetadataQueryDidFinishGatheringNotification object:self.query];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(processFiles:)
name:NSMetadataQueryDidUpdateNotification object:self.query];
[self.query startQuery];
}
The process files method provided by Apple is next:
- (void)processFiles:(NSNotification*)aNotification {
NSMutableArray *discoveredFiles = [NSMutableArray array];
//Always disable updates while processing results.
[query disableUpdates];
//The query reports all files found, every time.
NSArray *queryResults = [query results];
for (NSMetadataItem *result in queryResults) {
NSURL *fileURL = [result valueForAttribute:NSMetadataItemURLKey];
NSNumber *aBool = nil;
// Don't include hidden files.
[fileURL getResourceValue:&aBool forKey:NSURLIsHiddenKey error:nil];
if (aBool && ![aBool boolValue])
[discoveredFiles addObject:fileURL];
}
//Update the list of documents.
[FileList removeAllObjects];
[FileList addObjectsFromArray:discoveredFiles];
//[self.tableView reloadData];
//Reenable query updates.
[query enableUpdates];
NSLog(#"File List: %#", FileList);
}
Why doesn't this give me a list of files or at least some kind of data? Am I defining the NSMetadata query wrong, maybe my predicate isn't formatted right? I know I'm doing something wrong because there's no way iCloud could be this complicated (or could it?).
Thanks for the help in advance!
Edit #1: I am continuing to try different approaches to this problem. Using one of the answers below I have changed the predicate filter as follows:
[query setPredicate:[NSPredicate predicateWithFormat:#"NSMetadataItemFSNameKey LIKE '*'"]];
I have also added the following lines before the [query enableUpdates] call:
for (NSMetadataItem *item in query.results) {
[FileList addObject:[item valueForAttribute:NSMetadataItemFSNameKey]];
}
In the processFiles method, I've tried placing all of the code on the background thread, but this makes no difference - as a matter of fact, when the code is not executed on the background thread FileList gives me this (null) instead of this ( ).
Could my problem have to do with thread management or memory allocation? Please note that I am using ARC.
Edit #2: The FileList variable is an NSMutableArray defined in my #interface and initialized in the -(id)init method before calling the processFiles method.
Edit #3: When testing my code with breakpoints I found that the following for-loop never gets run through - not even once. This leads me to believe that:
A. The proper directory isn't being connected with
B. iCloud can't see the files in the directory
C. My NSMetadataQuery isn't being setup properly
D. Something completely different
Here's the code that starts the for-loop which never gets run:
NSArray *queryResults = [query results];
for (NSMetadataItem *result in queryResults) {
Since you already set the search scope correct, there's no need to use special filters in the predicate.
Just use:
query.predicate = [NSPredicate predicateWithFormat:#"NSMetadataItemFSNameKey == '*'"];
And to get the array use:
NSMutableArray *array = [NSMutableArray array];
for (NSMetadataItem *item in query.results) {
[array addObject:[item valueForAttribute:NSMetadataItemFSNameKey]];
}
I've solved my problem. Getting the list of files in iCloud was just a matter of correctly defining, allocating, and initializing properties. SAE's answer and this SO posting helped me solve my problem and create this GitHub Repo called iCloud Document Sync. The iCloud Document Sync class simplifies the whole iCloud Document storage process down to a few lines of code. The commit linked here fixes the issues from my question.

Deadlock in fetch routine

i am stuck with my first GCD and first core-data using application =)
two views access the same data (which is handled by a single DAO).
if i wait for the current view to finish loading its content no problem occors when changing view.
however: if i change the view (its tabbased) while one controller tries to fetch data from my model, the new controller tries the same and the threads 'collide' and my application freezes.
the freeze occurs in this line of code of my DAO:
NSArray *results = [managedObjectContext executeFetchRequest:fetch error:&error];
reloadAllMonth() accesses the fetch routine of my DAO
how i load the data in the first controller:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[self reloadAllMonth];
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self.allMonthTable reloadData];
});
in the second viewcontroller the first thing i do is update my DAO, this of course uses (beneath others) the very same fetch routine i called before:
[self.dataHandler updateData];
i have tried two approaches so far:
first using a c-semaphore:
-(NSArray *)fetchAllMonthExpenses{
//#return: array of all expenses in month (day & month type)
NSNumber *monthNumber = [self getMonthNumber:[NSDate date]];
NSEntityDescription *exp = [NSEntityDescription entityForName:#"Expense" inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetch = [[NSFetchRequest alloc]init];
[fetch setEntity:exp];
[fetch setPredicate:[NSPredicate predicateWithFormat:#"month == %#",monthNumber]];
NSError *error = nil;
sem_wait(&isLoading);
NSArray *results = [self.managedObjectContext executeFetchRequest:fetch error:&error];
sem_post(&isLoading);
return results;
}
second using the synchronized directive
-(NSArray *)fetchAllMonthExpenses{
//#return: array of all expenses in month (day & month type)
NSNumber *monthNumber = [self getMonthNumber:[NSDate date]];
NSEntityDescription *exp = [NSEntityDescription entityForName:#"Expense" inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetch = [[NSFetchRequest alloc]init];
[fetch setEntity:exp];
[fetch setPredicate:[NSPredicate predicateWithFormat:#"month == %#",monthNumber]];
NSError *error = nil;
#synchronized(self.class){
NSArray *results = [self.managedObjectContext executeFetchRequest:fetch error:&error];
return results;
}
}
sadly both of the approaches did not work, the application freezes whatever i do.
so my question is: what am i doing wrong (as i mentioned first time using threads), what am i missing, where should i look?
this has been keeping me busy for 2 days now and i cant seem to wrap my head around it :/
An NSManagedObjectContext and all of the NSManagedObjects inside it are not thread safe.
Whatever thread you use to create the context, that needs to be the only thread where you do anything relating to that context. Even just reading values from one of the managed object must be done on that thread and not on any other thread.
If you need two threads which both deal with the same database, you've got two options:
use dispatch_sync() to jump into the other thread momentarily to perform all read/write operations on the managed objects and/or the context
Or:
create a second NSManagedObjectContext in the other thread for the same database, and keep any changes made to the two contexts in sync.
The first option is much easier, but may remove much of the benefits of threading. The second option is harder, but it can be done, and there is a fairly good API for keeping two contexts on different threads in sync.
Lookup the Core Data Programming Guide for more details.

iOS store file in cache directory for set length of time

We have a POS apartment leasing iPad app that is used to collect a lot of data about a user and their interests (with their knowledge of course).
We use RestKit to sync CoreData with the server, which is totally sweet.
I'm using text files in the cache directory to store a history of their interactions with a guest card, such that it can be submitted in case of error, or sent to us via email, to recreate any guest card in case of some sort of syncing issue.
Although these should be very small text files, probably around 1-3k, I feel as though I should eventually clear these from the cache directory. (As I type this, maybe its so small I shouldn't worry about it).
I was curious if there was any way to clear files from the cache directory after a set amount of time? Say 90 days or so?
Word on the street is that if you use the sanctioned NSCachesDirectory location, if the OS needs that space, it'll delete things that are in that directory. I've never actually seen it happen in practice, but I've heard such things (and it stands to reason, otherwise why bother having special, OS-sanctioned location for cache files.)
That said, this task sounds pretty straightforward. Just fire off a low priority GCD background block to iterate through the files in that directory and delete any whose creation date was > 90 days ago. This is really easy if you only care about how long ago the data was created (as opposed to the last time you accessed the data which is harder to ascertain without keeping track yourself.) NSFileManager is your friend here. Something like this ought to work:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSFileManager* fm = [NSFileManager defaultManager];
NSMutableArray* urlsToDelete = [NSMutableArray array];
for (NSURL* dirUrl in [fm URLsForDirectory: NSCachesDirectory inDomains:NSUserDomainMask])
{
NSDirectoryEnumerator* dirEnum = [fm enumeratorAtURL: dirUrl
includingPropertiesForKeys: [NSArray arrayWithObject: NSFileModificationDate]
options: 0
errorHandler: ^(NSURL* a, NSError* b){ return (BOOL)YES; }];
NSURL* url = nil;
while ((url = [dirEnum nextObject]))
{
NSDate* modDate = [[dirEnum fileAttributes] objectForKey: NSFileModificationDate];
if (modDate && [[NSDate date] compare: [modDate dateByAddingTimeInterval: 60 * 60 * 24 * 90]] == NSOrderedDescending)
{
[urlsToDelete addObject: url];
}
}
}
for (NSURL* url in urlsToDelete)
{
[fm removeItemAtURL: url error: NULL];
}
});
To clarify, if you're looking for some mechanism by which to tell the OS 'delete this if I don't access it for more than 90 days' and have it keep track of this for you, I don't believe such a mechanism exists.