Here is a snippet of relevant code:
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Demo Document"];
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url];
if (document.documentState == UIDocumentStateClosed) {
NSLog(#"file is closed");
NSLog(#"%#",[url path]);
NSDate *start = [NSDate date];
[document openWithCompletionHandler:^(BOOL success) {
if (success) {
NSLog(#"finished OPEN");
NSDate *methodFinish = [NSDate date];
NSTimeInterval executionTime = [methodFinish timeIntervalSinceDate:start];
NSLog(#"time = %f",executionTime);
self.managedObjectContext = document.managedObjectContext;
}
}];
}
The time interval between "file is closed" and "finished open" was 16.6 seconds. Is this normal behavior? Bad coding? Or is it because I am running on a simulator?
I don't know if it would matter, but the file is barely 50KB...
This is not normal behavior. Have you tried clearing all iCloud data using Xcode 5 under "Debug->iCloud->Delete iCloud Contents"? It takes a few minutes to be fully realized but starting with a clean slate might help.
If you have already done this have you looked at what else might be working with that document that has a temporary hold on it? Or is something else blocking the main thread? Because that block won't execute until the main thread's event queue comes back around.
Believe it or not, this is normal. It isn't really anything to do with your code, but more or less how it is intended to work when you use autosave. This autosaving behavior is actually turned on/enabled by default ( you can also turn it off - but I would highly recommend not doing that unless you need to and know what you're doing heh).
Anyways, UIManagedDocument actually automatically saves your data to a document store for you by default and in the background no less - but the caveat to this is that it will do it whenever it can ( or better, whenever it wants to ). Realize that it does this in the background for you so that you don't have to worry about it ( its a feature I really love about it - but can also be a pain int he ass ).
It sounds like you're needing it to be saved because it isn't showing your data or you aren't seeing the results you would expect.
If that's the case, then I think you should look more into how to have it actually save your changes so that it can show up in your controllers. In reality, it really has very little to do with when UIManagedDocument saves anyways, and more about when your context is updated. If this is, indeed, the case you may find my answer to a different thread/question on this same subject helpful: Multiple UIManagedDocument For Read & Write
This may or may not be what you're after, but I believe it will get you on the right path at the very least.
Related
My app offers the option to download 3430 high resolution images from our server, each image of size 50k - 600k bytes.
The original approach was to just download all of them - but we realized that gave a lot of NSURLErrorTimedOut errors and crashed our program. We then implemented it such that we download all of the images, but in batches of 100 images at a time. Someone on SO suggested we actually implement our download like this:
Create a list of all file URLs that need to be downloaded.
Write your code so that it downloads these URLs sequentially. I.e. do
not let it start downloading a file until the previous one has
finished (or failed and you decided to skip it for now).
Use NSURLSession's support for downloading an individual file to a
folder, don't use the code to get an NSData and save the file
yourself. That way, your application doesn't need to be running while
the download finishes.
Ensure that you can tell whether a file has already been downloaded or
not, in case your download gets interrupted, or the phone is restarted
in mid-download. You can e.g. do this by comparing their names (if
they are unique enough), or saving a note to a plist that lets you
match a downloaded file to the URL where it came from, or whatever
constitutes an identifying characteristic in your case.
At startup, check whether all files are there. If not, put the missing
ones in above download list and download them sequentially, as in #2.
Before you start downloading anything (and that includes downloading
the next file after the previous download has finished or failed), do
a reachability check using the Reachability API from Apple's
SystemConfiguration.framework. That will tell you whether the user has
a connection at all, and whether you're on WiFi or cellular (in
general, you do not want to download a large number of files via
cellular, most cellular connections are metered).
We create a list of all images to download here:
- (void)generateImageURLList:(BOOL)batchDownloadImagesFromServer
{
NSError* error;
NSFetchRequest* leafletURLRequest = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription* leafletURLDescription = [NSEntityDescription entityForName:#"LeafletURL" inManagedObjectContext:managedObjectContext];
[leafletURLRequest setEntity:leafletURLDescription];
numberOfImages = [managedObjectContext countForFetchRequest:leafletURLRequest error:&error];
NSPredicate* thumbnailPredicate = [NSPredicate predicateWithFormat:#"thumbnailLocation like %#", kLocationServer];
[leafletURLRequest setPredicate:thumbnailPredicate];
self.uncachedThumbnailArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
NSPredicate* hiResPredicate = [NSPredicate predicateWithFormat:#"hiResImageLocation != %#", kLocationCache];
[leafletURLRequest setPredicate:hiResPredicate];
self.uncachedHiResImageArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
}
We use NSURLSession to download an individual image to a folder by calling hitServerForUrl and implementing didFinishDownloadingToURL:
- (void)hitServerForUrl:(NSURL*)requestUrl {
NSURLSessionConfiguration *defaultConfigurationObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfigurationObject delegate:self delegateQueue: nil];
NSURLSessionDownloadTask *fileDownloadTask = [defaultSession downloadTaskWithURL:requestUrl];
[fileDownloadTask resume];
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
if (isThumbnail)
{
leafletURL.thumbnailLocation = kLocationCache;
}
else
{
leafletURL.hiResImageLocation = kLocationCache;
}
// Filename to write to
NSString* filePath = [leafletURL pathForImageAtLocation:kLocationCache isThumbnail:isThumbnail isRetina:NO];
// If it's a retina image, append the "#2x"
if (isRetina_) {
filePath = [filePath stringByReplacingOccurrencesOfString:#".jpg" withString:#"#2x.jpg"];
}
NSString* dir = [filePath stringByDeletingLastPathComponent];
[managedObjectContext save:nil];
NSError* error;
[[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error];
NSURL *documentURL = [NSURL fileURLWithPath:filePath];
NSLog(#"file path : %#", filePath);
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
//Remove the old file from directory
}
[[NSFileManager defaultManager] moveItemAtURL:location
toURL:documentURL
error:&error];
if (error){
//Handle error here
}
}
This code calls loadImage, which calls `hitServer:
-(void)downloadImagesFromServer{
[self generateImageURLList:NO];
[leafletImageLoaderQueue removeAllObjects];
numberOfHiResImageLeft = [uncachedHiResImageArray count];
for ( LeafletURL* aLeafletURL in uncachedHiResImageArray)
{
//// Do the same thing again, except set isThumb = NO. ////
LeafletImageLoader* hiResImageLoader = [[LeafletImageLoader alloc] initWithDelegate:self];
[leafletImageLoaderQueue addObject:hiResImageLoader]; // do this before making connection!! //
[hiResImageLoader loadImage:aLeafletURL isThumbnail:NO isBatchDownload:YES];
//// Adding object to array already retains it, so it's safe to release it here. ////
[hiResImageLoader release];
uncachedHiResIndex++;
NSLog(#"uncached hi res index: %ld, un cached hi res image array size: %lu", (long)uncachedHiResIndex, (unsigned long)[uncachedHiResImageArray count]);
}
}
- (void)loadImage:(LeafletURL*)leafletURLInput isThumbnail:(BOOL)isThumbnailInput isBatchDownload:(BOOL)isBatchDownload isRetina:(BOOL)isRetina
{
isRetina_ = isRetina;
if (mConnection)
{
[mConnection cancel];
[mConnection release];
mConnection = nil;
}
if (mImageData)
{
[mImageData release];
mImageData = nil;
}
self.leafletURL = leafletURLInput;
self.isThumbnail = isThumbnailInput;
NSString* location = (self.isThumbnail) ?leafletURL.thumbnailLocation :leafletURL.hiResImageLocation;
//// Check if the image needs to be downloaded from server. If it is a batch download, then override the local resources////
if ( ([location isEqualToString:kLocationServer] || (isBatchDownload && [location isEqualToString:kLocationResource])) && self.leafletURL.rawURL != nil )
{
//NSLog(#"final loadimage called server");
//// tell the delegate to get ride of the old image while waiting. ////
if([delegate respondsToSelector:#selector(leafletImageLoaderWillBeginLoadingImage:)])
{
[delegate leafletImageLoaderWillBeginLoadingImage:self];
}
mImageData = [[NSMutableData alloc] init];
NSURL* url = [NSURL URLWithString:[leafletURL pathForImageOnServerUsingThumbnail:self.isThumbnail isRetina:isRetina]];
[self hitServerForUrl:url];
}
//// if not, tell the delegate that the image is already cached. ////
else
{
if([delegate respondsToSelector:#selector(leafletImageLoaderDidFinishLoadingImage:)])
{
[delegate leafletImageLoaderDidFinishLoadingImage:self];
}
}
}
Currently, I'm trying to figure out how to download the images sequentially, such that we don't call hitServer until the last image is finished downloading. Do I need to be downloading in the background? Thank you for suggestions!
My app offers the option to download 3430 high resolution images from our server, each image of size 50k - 600k bytes.
This seems like a job for on-demand resources. Just turn these files into on-demand resources obtained from your own server, and let the system take care of downloading them in its own sweet time.
This sounds very much like an architectural issue. If you fire off downloads without limiting them of course you're going to start getting timeouts and other things. Think about other apps and what they do. Apps that give the user the ability to do multiple downloads often limit how may can occur at once. iTunes for example can queue up thousands of downloads, but only runs 3 at a time. Limiting to just one at a time will only slow things down for your users. You need a balance that consider your user's available bandwidth.
The other part of this is to again consider what your users want. Does every one of your uses want every single image? I don't know what you are offering them, but in most apps which access resources like images or music, it's up to the user what and when they download. Thus they only download what they are interested in. So I'd recommend only downloading what the users are viewing or have somehow requested they want to download.
I am trying my hand at some very basic implementation of MagicalRecord to get the hang of it and run into the following.
When I save an entry and then fetch entries of that type it will come up with the entry I just saved. However, when I save the entry, close the app, start it again, and then fetch, it comes up empty.
Code for saving:
- (void)createTestTask{
NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];
Task *task = [Task createInContext:localContext];
task.tName = #"First Task";
task.tDescription = #"First Task created with MagicalRecord. Huzzah!";
NSError *error;
[localContext save:&error];
if (error != Nil) {
NSLog(#"%#", error.description);
}
}
Code for fetching: (all I want to know here if anything is actually saved)
- (void) fetchTasks{
NSArray *tasks = [Task findAll];
NSLog(#"Found %d tasks", [tasks count]);
}
I am sure I am missing something here, but not anything I can seem to find on stackoverflow or in the Tutorials I looked at.
Any help is welcome.
I have to ask the obvious "Is it plugged in" question: Did you initialize the Core Data Stack with one of the +[MagicalRecord setupCoreDataStack] methods?
Did your stack initialize properly? That is, is your store and model compatible? When they aren't, MagicalRecord (more appropriately, Core Data) will set up the whole stack without the Persistent Store. This is annoying because it looks like everything is fine until it cannot save to the store...because there is no store. MagicalRecord has a +[MagicalRecord currentStack] method that you can use to examine the current state of the stack. Try that in the debugger after you've set up your stack.
Assuming you did that, the other thing to check is the error log. If you use
[localContext MR_saveToPersistentStoreAndWait];
Any errors should be logged to the console. Generally when you don't see data on a subsequent run of your app, it's because data was not saved when you thought you called save. And the save, in turn, does not happen because your data did not validate correctly. A common example is if you have a required property, and it's still nil at the time you call save. "Normal" core data does not log these problems at all, so you might think it worked, when, in fact, the save operation failed. MagicalRecord, on the other hand, will capture all those errors and log them to the console at least telling you what's going on with your data.
When i have started with magical record I was also facing this problem, problem is context which you are using to save data. here is my code which might help you
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *userInfoArray = [UserBasicInfo findByAttribute:#"userId" withValue:[NSNumber numberWithInt:loggedInUserId] inContext:localContext];
UserBasicInfo* userInfo;
if ([userInfoArray count]) {
userInfo = [userInfoArray objectAtIndex:0];
} else {
userInfo = [UserBasicInfo createInContext:localContext];
}
userInfo.activeUser = [NSNumber numberWithBool:YES];
userInfo.firstName = self.graphUser[#"first_name"];
userInfo.lastName = self.graphUser[#"last_name"];
userInfo.userId = #([jsonObject[#"UserId"] intValue]);
userInfo.networkUserId = #([jsonObject[#"NetworkUserId"] longLongValue]);
userInfo.userPoint = #([jsonObject[#"PointsEarned"] floatValue]);
userInfo.imageUrl = jsonObject[#"Picturelist"][0][#"PictureUrL"];
userInfo.imageUrlArray = [NSKeyedArchiver archivedDataWithRootObject:jsonObject[#"Picturelist"]];
} completion:^(BOOL success, NSError *error) {
}];
Use this when your done
[[NSManagedObjectContext MR_defaultContext]saveToPersistentStoreAndWait];
I've been stuck on this for approximately two weeks. I hate posting things that have been asked a lot but I really have gone through them all.
I used Ray Wenderlich's tutorial for saving data in an iPhone app.
http://www.raywenderlich.com/tutorials
So that is the setup I have going on in my app. I'm saving very simple objects. My Card object consists of a name, type, and image. That's all. So the tutorial is quite close to mine. Which is making this more frustrating.
The thing is, I have some NSLog statements in there for loading. I have it displaying the folder it's using to load and what objects it does load. Right now it is displaying this.
Loading cards from /Users/zach/Library/Application Support/iPhone Simulator/7.0.3-64/Applications/E3DB01FD-A37E-4A69-840B-43830F2BDE2C/Library/Private Documents
2013-11-04 00:02:50.073 CardMinder[84170:a0b] ()
So it seems to be trying to load them, but there's nothing there to load. Here is my function to save data.
- (void)saveData {
if (_data == nil) return;
[self createDataPath];
NSString *dataPath = [_docPath stringByAppendingPathComponent:kDataFile];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:_data forKey:kDataKey];
[archiver finishEncoding];
NSLog(#"%#",dataPath);
NSLog(#"%#",data);
[data writeToFile:dataPath atomically:YES];
}
Which is really just what's posted in that tutorial. I know if you feel generous enough to help me out i'll have to post some more code but I don't want to flood the post with useless stuff so just let me know and i'll get it out here.
I really appreciate anyone that can help, I have recently entered the desperation state and need help.
Thanks
UPDATE
NSError *error;
[data writeToFile:dataPath options:NSDataWritingAtomic error:&error];
NSLog(#"error: %#", error.localizedFailureReason);
These are the methods for the CardData class. I'm doing the name, type, and a bool here.
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_name forKey:kNameKey];
[aCoder encodeObject:_cardType forKey:kTypeKey];
[aCoder encodeBool:_checkedOut forKey:kOutKey];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
NSString *name = [aDecoder decodeObjectForKey:kNameKey];
NSString *cardType = [aDecoder decodeObjectForKey:kTypeKey];
BOOL checkedOut = [aDecoder decodeBoolForKey:kOutKey];
return [self initWithName:name cardType:cardType _Bool:checkedOut];
}
UPDATE 2
I just put some more NSLog statements in and I found out that when I press the "Save card" button in my app, it doesn't seem to execute the saveData function at all. I have log statements galore in that saveData function and when I click the saveCard button it doesn't show any of those logs. Why would that be happening?
This is my saveButton code.
- (IBAction)saveNewCard:(id)sender
{
NSString *cardName = self.nameField.text;
_cardDoc.data.name = cardName;
CardDoc *newCard = [[CardDoc alloc] initWithName:cardName cardType:cardTypeString _Bool:NO image:chosenIcon];
[_cardDoc saveData];
NSLog(#"Card save button pressed!");
CardViewController *cardViewController = (CardViewController *)[self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2];
[cardViewController.cards addObject:newCard];
[self.navigationController popToRootViewControllerAnimated:YES];
}
You should use writeToFile:options:error: instead of writeToFile:atomically:; that will give you an error message that should prove helpful. (The equivalent to atomically:YES is the option constant NSDataWritingAtomic.) Make sure you're getting back a return value of YES; if not, the error should be set.
If you're getting a value of NO but the error is not set, it means you're messaging nil. A quirk of Objective-C is that messaging nil is completely valid. If the method is defined to return something, you'll even get a result: 0 or equivalent (NO, nil, etc.)
In this case, you're messaging _cardDoc. There's no return result to detect. This is a bit harder to defensively code around, but [_cardDoc saveData] is actually [nil saveData]. The debugger will just breeze past the line.
Generally, if something absolutely should not be nil, you can use NSAssert:
NSAssert(_cardData, #"_cardData should not be nil");
[_cardData saveData];
But use this sparingly; you'll probably come to usually appreciate this behaviour.
A few things.
Post the results of your log statements so we know what you are seeing.
In order for your approach to work, your _data object needs to conform to the NSCoding protocol. That means you need to add the protocol declaration to your interface, and implement the methods encodeWithCoder and initWithCoder.
In those methods you need to save all the state data for your object / load the state back into your object.
Those methods are the most likely source of problems with your code. Post those methods if you need help with them, and walk though them in the debugger.
You might also look at the NSKeyedArchvier class method archivedDataWithRootObject. That method takes an object and encodes it into an NSData object in one step. The method archiveRootObject:toFile: take it a step further, and writes the data directly to a file for you.
NSKeyedUnarchiver has the corresponding methods unarchiveObjectWithData and unarchiveObjectWithFile to recreate your object from data/a file.
My application, during a process I've written, is rising in memory and seems to not be releasing it.
The first thing I'd like to mention is that the basic outline of what I've written is this:
- Request a url (fetching data using NSData -initWithContentsOfURL)
- Parse NSData into NSArray of NSDictionarys using NSJSONSerialization +JSONObjectWithStream
- Loop through decoded NSArray inserting/updating/deleting records in a sqlite database using the FMDB framework with the data decoded
The application does the above, however it does it in a loop for an undetermined period of time, in which the application displays a "Loading" HUD. I thought it may be worth mentioning this, although I find it insignificant how many times it does this process, as that shouldn't affect the memory usage if it were releasing properly. If I am wrong here, please advise me.
My code works fine, well, it does what it's intended to do. However, when I profile the application code, the memory seems to just keep rising. It does drop in segments throughout, but overall it keeps rising (IE doesn't release in full what it previously used).
I have, as previously stated, profiled the application with Allocations, Leaks, VM Tracker, and used Trace Highlights.
Trace Highlights: Shows that the memory usage is gradually going up, but dropping some memory (not all) meaning if the process is running for long enough the memory will reach high usage and terminate.
Allocations: Seems OK. The allocations has spikes but always comes back down to where it started. I took heapshots and they always drop down leaving maximum 500-700kb per segment (left for about 10 minutes)
VM Tracker: Proves to show that memory consistently rises, and is not releasing full memory (as discovered in trace highlights). Resident seems to get really high
Leaks: No leaks found in application
Here's some screenshots of Allocations/VM Tracker running:
It's worth noting that I have in fact tried:
- Adding autoreleasepools
- "force releasing" by assigning each properties; such as NSURLs, NSRequests, etc; to nil
My questions:
- Should I be doing something special to release the memory?
- How could I further debug this issue?
- How best can I find out what's wrong from the data Instruments gives me?
---- EDIT: ----
Here's the code that sends the url request to fetch the data.:
- (void) requestAndParse : (NSString *)url
{
NSURL *theURL;
ASIHTTPRequest *request;
NSData *collectedData;
NSError *error;
#try {
// File cache the NSData
theURL = [[NSURL alloc] initWithString: url];
request = [ASIHTTPRequest requestWithURL: theURL];
[request setDownloadDestinationPath: [[NSHomeDirectory() stringByAppendingPathComponent:#"Documents"] stringByAppendingString:#"/cachefile.txt"]];
[request startSynchronous];
[request waitUntilFinished];
collectedData = [[NSData alloc] initWithContentsOfFile:[[NSHomeDirectory() stringByAppendingPathComponent:#"Documents"] stringByAppendingString:#"/cachefile.txt"]];
if ([collectedData length] > 0) {
records = [NSJSONSerialization JSONObjectWithData:collectedData options:NSJSONReadingMutableContainers error:&error];
}
}
#catch (NSException *exception) {
// Failed
NSLog(#"Parse error: %#", error);
}
#finally {
// DB updates with the records here
...
// remove file
[[NSFileManager defaultManager] removeItemAtPath:[[NSHomeDirectory() stringByAppendingPathComponent:#"Documents"] stringByAppendingString:#"/cachefile.txt"] error:nil];
// release properties used
collectedData = nil;
request = nil;
theURL = nil;
}
}
This above method is called from within a while loop in the Application Delegate. The while loop is an undetermined length, as previously mentioned.
--- EDIT 2: ---
The following is what happens within the #finally statement (updating the SQLite database using FMDB). There are a lot of these methods in my class, one for each table. They all follow the same pattern though, as they are all duplicated from the first one:
-(BOOL) insertBatchOfRecords:(NSArray *)records {
__block BOOL queueReturned = YES;
#autoreleasepool {
FMDatabaseQueue *dbQueue = [self instantiateDatabaseQueue];
[dbQueue inTransaction:^(FMDatabase *tdb, BOOL *rollback) {
if (![tdb open]) {
NSLog(#"Couldn't open DB inside Transaction");
queueReturned = NO;
*rollback = YES;
return;
}
for (NSDictionary *record in records) {
[tdb executeUpdate:#"INSERT OR REPLACE INTO table (attr1, attr2) VALUES (?,?)", [record valueForKey:#"attr1"], [record valueForKey:#"attr2"]];
if ([tdb hadError]) {
queueReturned = NO;
*rollback = YES;
NSLog(#"Failed to insert records because %#", [tdb lastErrorMessage]);
return;
}
}
}];
[dbQueue close];
dbQueue = nil;
}
return queueReturned;
}
And follows is the -instantiateDatabaseQueue method:
-(FMDatabaseQueue *) instantiateDatabaseQueue {
#autoreleasepool {
return [FMDatabaseQueue databaseQueueWithPath: [self.getDocumentsDirectory stringByAppendingPathComponent:#"localdb.db"]];
}
}
The autoreleasepools may make it messy, but the code originally did not have these. I implemented them in various locations to see if there was any improvement (there was not).
--- EDIT 3 ---
I have been profiling the application the past few days, and still have no luck in finding an answer. I have separated the part of the app in question to a separate project of it's own, to make sure that it is indeed this causing the memory usage. This proved to be correct, as the app is still acting the same.
I have taken further profiling pictures, and am still having a hard time identifying what is actually wrong. See below that the allocations looks OK (the VM also doesn't look too bad to me?), and there's still no leaks (no picture of this, because there's none!!)
However, when I profiled on Trace Highlights, the memory usage just keeps going up, until reaching too much usage (around 70+MB on a 3GS) and then crashes due to using so much memory.
I reduced the problem by using ASIHTTPRequest for grabbing the NSData (stores to file instead). Please see revised code above. However, problem still persists, just takes longer to happen!
As per originally, question:
- Is there something wrong with the second part of this app process?
Using try / catch in iOS with ARC can cause memory leaks, and is best avoided.
an alternative approach is to use an async NSURLConnection, or an NSOperation with a synch NSURLConnection.
I have noticed that when I call
[context save:nil];
the saving doesn't occur instantly. I tested that when I try to save and quit the app in one or two seconds. It only works if I keep the app open for 5+ seconds or so.
I have 2 questions:
How can I know when the save is complete? A simple NSLog() will be enough, just for testing purposes.
Can I force a save? Should I?
Testing if [context save] is synchronous
I have tested that many times and this is not the behavior that I'm getting. If I have this code:
[context save:nil]
NSLog(#"Saved");
I see the "Saved" log, quit the app, and when I launch it again and try to fetch the data - nothing there. This doesn't happen if I wait about 5-10 seconds after I see the "Saved" message.
Thoughts?
Some code
- (void)storeSales:(NSArray *)sales {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"EE LLLL d HH:mm:ss Z YYYY"];
for (NSDictionary *saleDictionary in sales) {
Sale *sale = [NSEntityDescription insertNewObjectForEntityForName:#"Sale" inManagedObjectContext:self.context];
sale.productName = [saleDictionary objectForKey:#"description"];
sale.date = [formatter dateFromString:[saleDictionary objectForKey:#"occured_at"]];
NSLog(#"Stored new sale in database.");
}
[self.context save:nil];
}
How I setup the UIManagedDocument
#property (nonatomic, strong) UIManagedDocument *document;
#property (nonatomic, strong) NSManagedObjectContext *context;
...
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"SalesBot Database"];
self.document = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {}];
} else if (self.document.documentState == UIDocumentStateClosed) {
[self.document openWithCompletionHandler:^(BOOL success) {}];
}
self.context = self.document.managedObjectContext;
UPDATE 1
I tried using NSNotificationCenter to receive NSManagedObjectContextDidSaveNotification - but - I'm receiving it twice! Once right after [context save:nil] and again 5-10 seconds later!
So it seems like you are using UIManagedDocument above Core Data. This explains some things.
At first, use -[UIDocument saveToURL:forSaveOperation:completionHandler:] to save your document. From docs:
You should typically use the standard UIDocument methods to save the document.
If you save the child context directly, you only commit changes to the parent context and not to the document store. If you save the parent context directly, you sidestep other important operations that the document performs.
UIManagedDocument works with two managed object contexts. One is working on main thread, the second is saving changes to file in background thread. This is why your changes were saved, but after reopening were lost. The second context did not finish save operation.
This explains also why your notification was triggered two times. One for each context.
The save method is not an asynchronous process. Anything that occurs after you call the save method will be executed after it has saved.
NSLog(#"About to force a save...");
[context save:nil];
NSLog(#"Now I know the save is complete!");
You can observe notification NSManagedObjectContextDidSaveNotification.