Reputation: 2706
I am writing an app targeting iOS7.
My testing devices are:
1) an iPhone 5 16GB
2) an iPad 3rd generation 16GB Wi-fi+Cellular.
I am getting mad with UIManagedDocument and iCloud.
I read all the documentation I found, I watched the Stanford CS193P videos and I am following some code by Erica Sadun.
The problem I am not able to solve is that after I create the local CoreData file I can't reopen it or save it because it is in [Closed | SavingError] state.
It is very important to me to have it open and ready because:
1) The first time the user launches the app, it fills the DB with demo data.
2) After the demo data creation the user can create some new data but if the file is in this state it can't save the data created.
3) If the document is in SavingError state the demo data won't be saved.
The first TableViewController in the app calls fetchDataWithBlock:.The block is a simply, standard NSFetchRequest.
This is the code I'm using to set the PSC options for the UIManagedDocument, and to create and open it as suggested by Erica (create, close, reopen). There is a lot of logging in it, to better understand what's going on.
#define DB_LOCAL_FILE_NAME @"CoreDataLocalFile"
#define DB_TRANSACTIONS_LOG_FILE_NAME @"TransactionsLog"
-(NSURL *) dbLocalDirectory
{
//Returns the application's document directory
_dbLocalDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
return _dbLocalDirectory;
}
-(NSURL*) iCloudURL
{
_iCloudURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
return _iCloudURL;
}
-(NSURL*) iCloudDataLogFilesURL
{
_iCloudDataLogFilesURL = [self.iCloudURL URLByAppendingPathComponent:@"CoreDataTransactionsLog"];
//If the folder is not present, create it.
if(![[NSFileManager defaultManager] fileExistsAtPath:_iCloudDataLogFilesURL.path]){
NSLog(@"Creating the iCloudDataLogFilesURL: %@", _iCloudDataLogFilesURL);
NSError *error;
if(![[NSFileManager defaultManager] createDirectoryAtPath:_iCloudDataLogFilesURL.path withIntermediateDirectories:YES attributes:nil error:&error]){
NSLog(@"ERROR creating iCloud folder: %@", error.localizedFailureReason);
}
}
return _iCloudDataLogFilesURL;
}
-(UIManagedDocument *) managedDocument
{
//Returns the database ManagedDocument
if (!_managedDocument){
//Init the document
_managedDocument = [[UIManagedDocument alloc] initWithFileURL:
[self.dbLocalDirectory URLByAppendingPathComponent:DB_LOCAL_FILE_NAME]];
}
return _managedDocument;
}
-(void) setPersistentStoreOptionsInDocument: (UIManagedDocument*) document
{
if(self.iCloudDataLogFilesURL){
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:DB_TRANSACTIONS_LOG_FILE_NAME
forKey:NSPersistentStoreUbiquitousContentNameKey];
[options setObject:self.iCloudDataLogFilesURL
forKey:NSPersistentStoreUbiquitousContentURLKey];
[options setObject:[NSNumber numberWithBool:YES]
forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:[NSNumber numberWithBool:YES]
forKey:NSInferMappingModelAutomaticallyOption];
document.persistentStoreOptions = options;
//if file exists, use contents of document.fileURL/@"DocumentsMetadata.plist" instead
NSLog(@"Using iCLoud with PSC Options: %@", document.persistentStoreOptions);
}else{
NSLog(@"ERROR. Can't add iCloud options because I don't have a valid iCloudDataLogFilesURL.");
}
}
-(void) fetchDataWithBlock: (void (^) (void)) fetchingDataBlock
{
//If the CoreData local file exists then open it and perform the query
if([[NSFileManager defaultManager] fileExistsAtPath:[self.managedDocument.fileURL path]]){
NSLog(@"The CoreData local file in the application sandbox already exists. I am opening it.");
[self.managedDocument openWithCompletionHandler:^(BOOL success) {
if(success){
NSLog(@"SUCCESS: The CoreData local file has been opened succesfully. Fetching data.");
fetchingDataBlock();
}else{
NSLog(@"ERROR: Can't open the CoreData local file. Can't fetch the data.");
NSLog(@"%@", self.managedDocument);
return;
}
}];
}else{
NSLog(@"The CoreData local file in the application sandbox did not exist.");
//1. Create the Core Data local File
//----> Set the iCloud options
[self setPersistentStoreOptionsInDocument:_managedDocument];
[self.managedDocument saveToURL:self.managedDocument.fileURL
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
if(success){
//2. Close the Core Data local File
NSLog(@"SUCCESS: I created the CoreData local file in the application sandbox. Now I am closing it.");
[self.managedDocument closeWithCompletionHandler:^(BOOL success) {
if(success){
//3. Reopen the Core Data local File
NSLog(@"SUCCESS: I closed the CoreData local file just created. Now I am reopening it.");
[self.managedDocument openWithCompletionHandler:^(BOOL success) {
if(success){
NSLog(@"SUCCESS: I reopened the CoreData local file just created. Fetching data.");
fetchingDataBlock();
}else{
NSLog(@"ERROR: Can't reopen the CoreData local file just created and then closed. Can't fetch the data.");
NSLog(@"%@", self.managedDocument);
return;
}
}];
}else{
NSLog(@"ERROR: Can't close the CoreData local file just created. Can't fetch the data.");
NSLog(@"%@", self.managedDocument);
return;
}
}];
}else{
NSLog(@"ERROR: Can't create the CoreData local file in the application sandbox. Can't fetch the data.");
NSLog(@"%@", self.managedDocument);
return;
}
}];
}
}
These are the logs:
2013-11-16 18:19:18.731 Current iCloud Token:
2013-11-16 18:19:19.001 Using iCLoud with PSC Options: { NSInferMappingModelAutomaticallyOption = 1; NSMigratePersistentStoresAutomaticallyOption = 1; NSPersistentStoreUbiquitousContentNameKey = "TransactionsLog"; NSPersistentStoreUbiquitousContentURLKey = "file:///private/var/mobile/Library/Mobile%20Documents/XXX/CoreDataTransactionsLog/"; }
2013-11-16 18:19:19.003 The CoreData local file in the application sandbox did not exist.
2013-11-16 18:19:19.032 ApplicationDidBecomeActive
2013-11-16 18:19:19.313 -PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:: CoreData: Ubiquity: mobile~D5AEDBB6-EEFC-455C-A52C-91ADDC1BE081:TransactionsLog Using local storage: 1
2013-11-16 18:19:19.771 SUCCESS: I created the CoreData local file in the application sandbox. Now I am closing it.
2013-11-16 18:19:19.778 SUCCESS: I closed the CoreData local file just created. Now I am reopening it.
2013-11-16 18:19:20.073 ERROR: Can't reopen the CoreData local file just created and then closed. Can't fetch the data.
2013-11-16 18:19:20.074 fileURL: file:///var/mobile/Applications/07702036-765D-414C-9E8B-D4C2B4055CB8/Documents/CoreDataLocalFile documentState: [Closed | SavingError]
As you can see the documentState is: [Closed | SavingError]
If I restart the app, it opens the DB without problems, the document state is NORMAL. The problem is that I need it open and ready the first time I run the app and I create the demo data. Then the file has to be immediately saved witht the UIDocumentsaveForOverwriting option.
I can't ask the user to quit the app and then restart it or losing all the dem data because the file won't be saved!
To better understand what's going on I subclassed UIManagedDocument to override handleError:userInteractionPermitted:.
It tells me the document has this error state because "Error Domain=NSCocoaErrorDomain Code=134311 "The provided ubiquity name is already in use.".
2013-11-17 18:33:50.214 [370:1803] ERROR in UIManagedDocument: Error Domain=NSCocoaErrorDomain Code=134311 "The provided ubiquity name is already in use." UserInfo=0x14edadf0 {NSLocalizedDescription=The provided ubiquity name is already in use., NSURL=file:///var/mobile/Applications/A0B371E0-C992-4D57-895A-E2CCB8A35367/Documents/CoreDataLocalFile/StoreContent.nosync/CoreDataUbiquitySupport/mobile~0EDD3A67-63F4-439F-A055-A13808949BBD/TransactionsLog/A9728F87-0F79-4FE3-9B76-AABD3950BB67/store/persistentStore, NSPersistentStoreUbiquitousContentNameKey=TransactionsLog}
Additional Infos:
if I move:
//Set the iCloud options
[self setPersistentStoreOptionsInDocument:_managedDocument];
from before to after the local file creation with the method saveToURL:forSaveOperation:completionHandler: then the UIManagedDocument reopens correctly but it triggers an error code 134030 whenever I try to save it using the same method used for creation but with the value UIDocumentSaveForOverwriting for the forSaveOperation option. The same happens also if instead to overwrite the file with saveToURL:forSaveOperation:completionHandler: I use [self.managedDocument.managedObjectContext save:nil]
Note 1: I commented out all the demo data creation code to be sure it was not the one to blame. As soon as the blank UIManagedDocument is reopened succesfully after its creation I try to save it overwriting.
Note 2: Getting the options from the UIManagedDocument (local file) DocumentMetaData.plist shows that the only option set is NSPersistentStoreUbiquitousContentNameKey. This is weird because I set 4 different options after the file creation.
Any hints on how to fix this?
Thanks
Nicola
Upvotes: 0
Views: 843
Reputation: 2706
Finally I fixed it! Maybe something internal in CoreData/UIManagedDocument has changed in the last two years, since Erica's demo code. I simplified the creation/opening/fetching of data not using the close document and subsequent reopening routines. Now the code works.
-(void) fetchDataWithBlock: (void (^) (void)) fetchingDataBlock
{
//If the CoreData local file exists then open it and perform the query
if([[NSFileManager defaultManager] fileExistsAtPath:[self.managedDocument.fileURL path]]){
NSLog(@"The CoreData local file in the application sandbox already exists.");
if (self.managedDocument.documentState == UIDocumentStateNormal){
NSLog(@"The CoreData local file it's in Normal state. Fetching data.");
fetchingDataBlock();
}else if (self.managedDocument.documentState == UIDocumentStateClosed){
NSLog(@"The CoreData local file it's in Closed state. I am opening it.");
[self.managedDocument openWithCompletionHandler:^(BOOL success) {
if(success){
NSLog(@"SUCCESS: The CoreData local file has been opened succesfully. Fetching data.");
fetchingDataBlock();
}else{
NSLog(@"ERROR: Can't open the CoreData local file. Can't fetch the data.");
NSLog(@"%@", self.managedDocument);
return;
}
}];
}else{
NSLog(@"ERROR: The CoreData local file has an unexpected documentState: %@", self.managedDocument);
}
}else{
NSLog(@"The CoreData local file in the application sandbox did not exist.");
NSLog(@"Setting the UIManagedDocument PSC options.");
[self setPersistentStoreOptionsInDocument:self.managedDocument];
//Create the Core Data local File
[self.managedDocument saveToURL:self.managedDocument.fileURL
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
if(success){
NSLog(@"SUCCESS: The CoreData local file has been created. Fetching data.");
fetchingDataBlock();
}else{
NSLog(@"ERROR: Can't create the CoreData local file in the application sandbox. Can't fetch the data.");
NSLog(@"%@", self.managedDocument);
return;
}
}];
}
}
Upvotes: 0
Reputation: 8988
I think I did post this once before but here it is again. A couple of points to note:
// This gets called when the user has done one of the following: // 1. Created a new file and entered a new file name. We have then created the fileURL // using the /Documents directory, the filename and appending 'UUID'+uuid to ensure that // avoid duplicate file names in case the user used the same file name on another device. // 2. Selected an existing file from the file browser // - (void)createNewFile:(NSURL*)fileURL {
//FLOG(@"createNewFile called with url %@", fileURL);
_creatingNewFile = YES; // Ignore any file metadata scan events coming in while we do this because some iCloud
// files get created by Core Data before the local files are created and our scanning
// picks up new iCloud files and attempts to create local copies and we don't want this
// if this devices is busy creating the new iCloud file
_document = [[OSManagedDocument alloc] initWithFileURL:fileURL];
// Set oberving on this file to monitor the state (we don't use it for anything other than debugging)
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(documentStateChanged:)
name:UIDocumentStateChangedNotification
object:_document];
_openedVersion = [NSFileVersion currentVersionOfItemAtURL:fileURL];
_openedVersionDate = _openedVersion.modificationDate;
_openedVersionDevice = _openedVersion.localizedNameOfSavingComputer;
//FLOG(@" file version date: %@", _openedVersionDate);
//FLOG(@" file version device: %@", _openedVersionDevice);
NSString *fileName = [[fileURL URLByDeletingPathExtension] lastPathComponent];
[_document setPersistentStoreOptions:@{NSPersistentStoreUbiquitousContentNameKey:fileName,
NSMigratePersistentStoresAutomaticallyOption:@YES,
NSInferMappingModelAutomaticallyOption:@YES,
NSSQLitePragmasOption:@{ @"journal_mode" : @"DELETE" }}];
_managedObjectContext = _document.managedObjectContext;
if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) {
//FLOG(@" file exists so open it: %@",fileURL);
[_document openWithCompletionHandler:^(BOOL success){
if (!success) {
// Handle the error.
LOG(@" error creating file");
} else {
//LOG(@" file opened");
[self fileOpened]; // Now initialise the UI and let the user continue...
}
}];
}
else {
// File does not exist so that means the user has created a new one and we need to
// load some initialisation data into the Core Data store (codes tables, etc.)
//
// At this stage we have a database in memory so we can just use the _document.managedObjectContext
// to add objects prior to attempting to write to disk.
//LOG(@" file DOES NOT exist so add initial data");
[self addInitialData];
// Just checking if anything has been written to disk, nothing should not exist on disk yet.
// Debugging use only
if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]])
LOG(@" file exists but keep going anyway :-(");
else
LOG(@" file still does not exist :-)");
// OK now save a copy to disk using UIManagedDocument
// NOTE: the iCloud files are written before the UIManagedDocument.fileURL, presumably because Core Data does this setup
// in response to the [moc save:]. Make sure we don't pick this up in our iCloud metaData scan and attempt to create
// it as if it were a new iCloud file created by some other device.
//
[_document saveToURL:_document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success){
if (!success) {
// Handle the error.
LOG(@" error saving file :-(");
}
// We close the file and wait for it to appear in the file browser and then
// let the user select it from the browser to open it and start using it.
// Skip this if you want to open it directly and [self fileopened] but
// bear in mind the fileListController will not be correctly set up when we return to it.
[_document closeWithCompletionHandler:^(BOOL success){
if (!success) {
// Handle the error.
LOG(@" error closing file after creation :-(");
FLOG(@" file URL is %@", [fileURL path]);
} else {
FLOG(@" file closed %@", fileName);
_creatingNewFile = NO; // OK we are done, so let metaData scanning go ahead as normal
// Tell our UITableView file list that we are done and trigger scanning of local and iCloud files
// The fileListController will the add the new file itself and the user will then pick the
// file from this list in order to open it.
// To open the file automatically use the callback in the fileListController
// to select and then open the file so it looks seamless to the user.
[self.fileListController fileHasBeenCreated:fileURL];
// Stop observing now
[center removeObserver:self
name:UIDocumentStateChangedNotification
object:_document];
}
}];
}];
}
}
Upvotes: 1
Reputation: 8988
It hard to understand what your code is doing but it seems to be a lot more complicated than necessary to just open a new or existing core data file using the UIManagedDocument wrapper.
One strange thing is the following:
NSPersistentStoreUbiquitousContentNameKey=TransactionsLog (see your last line)
You shouldn't be setting this to the transaction log path. It should be a unique iCloud store name.
Also its not clear why you need a URL to the transaction log path - I never use this nor do I need to set the transaction log path because Core Data does this for you. You only need it if you have legacy apps which use custom log paths.
I'll post some of my code for creating the UIManagedDocument here in a few minutes.
Upvotes: 0