@protocol OSManagedDocumentDelegate <NSObject>
@required
- (void)fileMigrated:(NSURL*)migratedURL success:(bool)success;
@end
@interface OSManagedDocument : UIManagedDocument
@property (nonatomic, weak) id<OSManagedDocumentDelegate> delegate;
- (void)moveDocumentToICloud;
- (void)moveDocumentToLocal;
@end
@implementation OSManagedDocument
- (id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
LOG(@"Auto-Saving Document");
return [super contentsForType:typeName error:outError];
}
- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted
{
FLOG(@" error: %@", error.localizedDescription);
NSArray* errors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(errors != nil && errors.count > 0) {
for (NSError *error in errors) {
FLOG(@" Error: %@", error.userInfo);
}
} else {
FLOG(@" error.userInfo = %@", error.userInfo);
}
}
// The name of the file that contains the store identifier.
static NSString *DocumentMetadataFileName = @"DocumentMetadata.plist";
// The name of the file package subdirectory that contains the Core Data store when local.
static NSString *StoreDirectoryComponentLocal = @"StoreContent";
// The name of the file package subdirectory that contains the Core Data store when in the cloud. The Core Data store itself should not be synced directly, so it is placed in a .nosync directory.
static NSString *StoreDirectoryComponentCloud = @"StoreContent.nosync";
static NSString *StoreFileName = @"persistentStore";
- (NSDictionary *)optionsForStoreAtURL:(NSURL *)url {
NSURL *metadataDictionaryURL = [url URLByAppendingPathComponent:DocumentMetadataFileName];
NSDictionary __block *storeMetadata = nil;
/*
Perform a coordinated read of the store metadata file; the coordinated read ensures it is downloaded in the event that the document is cloud-based.
*/
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[fileCoordinator coordinateReadingItemAtURL:metadataDictionaryURL options:0 error:NULL byAccessor:^(NSURL *newURL) {
storeMetadata = [[NSDictionary alloc] initWithContentsOfURL:newURL];
}];
NSString *persistentStoreUbiquitousContentName = nil;
if (storeMetadata != nil) {
persistentStoreUbiquitousContentName = [storeMetadata objectForKey:NSPersistentStoreUbiquitousContentNameKey];
if (persistentStoreUbiquitousContentName == nil) {
// Should not get here.
NSLog(@"ERROR in optionsForStoreAtURL:");
NSLog(@"persistentStoreUbiquitousContentName == nil");
abort();
}
}
else {
CFUUIDRef uuid = CFUUIDCreate(NULL);
CFStringRef uuidString = CFUUIDCreateString(NULL, uuid);
persistentStoreUbiquitousContentName = (__bridge_transfer NSString *)uuidString;
CFRelease(uuid);
}
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
persistentStoreUbiquitousContentName, NSPersistentStoreUbiquitousContentNameKey,
nil];
return options;
}
/*! Moves a local document to iCloud by creating a new UIManagedDocument using the local filename+"_UUID_"+uuid
in the same directory and then migrating the core data store to this document with iCloud options. When completed
makes a callback to the cloudManager so the cloudManager can delete the local file.
@param cloudManager The cloud file manager that get the callback when the file migration has completed successfully.
*/
- (void)moveDocumentToICloud {
FLOG(@"moveDocumentToICloud called");
NSURL * sourceDocumentURL = self.fileURL;
NSURL * destination = [sourceDocumentURL URLByDeletingLastPathComponent];
// Create the new iCloud filename
NSString *documentName = [NSString stringWithFormat:@"%@_UUID_%@",[sourceDocumentURL lastPathComponent], [[NSUUID UUID] UUIDString]];
NSURL *destinationURL = [destination URLByAppendingPathComponent:documentName];
NSURL *sourceStoreURL = [[sourceDocumentURL URLByAppendingPathComponent:StoreDirectoryComponentLocal isDirectory:YES] URLByAppendingPathComponent:StoreFileName isDirectory:NO];
NSDictionary *localOptions = @{NSMigratePersistentStoresAutomaticallyOption:@YES,
NSInferMappingModelAutomaticallyOption:@YES,
NSSQLitePragmasOption:@{ @"journal_mode" : @"DELETE" }};
NSDictionary *ubiquityOptions = @{NSPersistentStoreUbiquitousContentNameKey:documentName,
NSMigratePersistentStoresAutomaticallyOption:@YES,
NSInferMappingModelAutomaticallyOption:@YES,
NSSQLitePragmasOption:@{ @"journal_mode" : @"DELETE" }};
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtURL:destinationURL error:nil];
__block bool moveSuccess = NO;
// Now create a new UIManagedDocument which will use the model from the application bundle
OSManagedDocument *newDocument = [[OSManagedDocument alloc] initWithFileURL:destinationURL];
newDocument.persistentStoreOptions = ubiquityOptions;
[newDocument saveToURL:destinationURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success){
if (!success) {
// Handle the error.
LOG(@" error saving file :-(");
} else {
// Now that we have created a new OSManagedDocument remove the store and migrate the local store
NSPersistentStoreCoordinator *pscForSave = newDocument.managedObjectContext.persistentStoreCoordinator;
NSPersistentStore *store = [[pscForSave persistentStores] objectAtIndex:0];
NSURL *storesURL = store.URL;
// Now remove the existing store
NSError *removeStoreError;
bool result = [pscForSave removePersistentStore:store error:&removeStoreError];
if (!result) {
FLOG(@" Failed to remove store: %@, %@", removeStoreError, removeStoreError.userInfo);
} else {
id sourceStore = [pscForSave addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sourceStoreURL options:localOptions error:nil];
if (!sourceStore) {
FLOG(@" failed to add old store");
} else {
NSError *error;
id migrationSuccess = [pscForSave migratePersistentStore:sourceStore toURL:storesURL options:ubiquityOptions withType:NSSQLiteStoreType error:&error];
if (migrationSuccess) {
moveSuccess = YES;
FLOG(@"store successfully migrated");
}
else {
FLOG(@"Failed to migrate store: %@, %@", error, error.userInfo);
moveSuccess = NO;
}
}
}
}
[newDocument closeWithCompletionHandler:^(BOOL success){
if (!success) {
// Handle the error.
LOG(@" error closing file after creation :-(");
} else {
FLOG(@" file closed");
}
// Async callback to CloudManager to tell it we are done migrating
[self.delegate fileMigrated:sourceDocumentURL success:moveSuccess];
}];
}];
return;
}
/*! Moves an iCloud document to local by creating a new UIManagedDocument using the filename component of the iCloud ubiquity name (filename+"_UUID_"+uuid)
in the same directory and then migrating the core data store to this document without iCloud options and removes the store from iCloud and finally
deletes the local copy.
Note that even if it fails to remove the iCloud files it deletes the local copy. User may need to clean up orphaned iCloud files using a Mac!
@return Returns YES of file was migrated or NO if not.
*/
- (void)moveDocumentToLocal {
NSURL * sourceDocumentURL = self.fileURL;
NSString *ubiquityNameKey = [sourceDocumentURL lastPathComponent];
NSURL * destination = [sourceDocumentURL URLByDeletingLastPathComponent];
// Create the new local filename
NSString *documentName = [[[sourceDocumentURL lastPathComponent] componentsSeparatedByString:@"_UUID_"] objectAtIndex:0];
NSURL *destinationURL = [destination URLByAppendingPathComponent:documentName];
NSURL *sourceStoreURL = [self storeFileURL2];
if (sourceStoreURL == nil) {
FLOG(@" error unable to find sourceStoreURL");
return;
}
NSDictionary *localOptions = @{NSPersistentStoreRemoveUbiquitousMetadataOption:@YES,
NSMigratePersistentStoresAutomaticallyOption:@YES,
NSInferMappingModelAutomaticallyOption:@YES,
NSSQLitePragmasOption:@{ @"journal_mode" : @"DELETE" }};
NSDictionary *ubiquityOptions = @{NSPersistentStoreUbiquitousContentNameKey:ubiquityNameKey,
NSMigratePersistentStoresAutomaticallyOption:@YES,
NSInferMappingModelAutomaticallyOption:@YES,
NSSQLitePragmasOption:@{ @"journal_mode" : @"DELETE" }};
// Remove any local file
// We should check if a filename exists and then create a new filename using a counter...
// because its possible to have more than one file with the same user filename in iCloud
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtURL:destinationURL error:nil];
__block bool moveSuccess = NO;
// Now create a new UIManagedDocument which will use the model from the application bundle
OSManagedDocument *newDocument = [[OSManagedDocument alloc] initWithFileURL:destinationURL];
newDocument.persistentStoreOptions = localOptions;
[newDocument saveToURL:destinationURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success){
if (!success) {
// Handle the error.
LOG(@" error saving file :-(");
} else {
// Now that we have created a new OSManagedDocument remove the store and migrate the local store
NSPersistentStoreCoordinator *pscForSave = newDocument.managedObjectContext.persistentStoreCoordinator;
NSPersistentStore *store = [[pscForSave persistentStores] objectAtIndex:0];
NSURL *storesURL = store.URL;
// Now remove the existing store
NSError *removeStoreError;
bool result = [pscForSave removePersistentStore:store error:&removeStoreError];
if (!result) {
FLOG(@" Failed to remove store: %@, %@", removeStoreError, removeStoreError.userInfo);
} else {
id sourceStore = [pscForSave addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sourceStoreURL options:ubiquityOptions error:nil];
if (!sourceStore) {
FLOG(@" failed to add old store");
} else {
NSError *error;
id migrationSuccess = [pscForSave migratePersistentStore:sourceStore toURL:storesURL options:localOptions withType:NSSQLiteStoreType error:&error];
if (migrationSuccess) {
FLOG(@"store successfuly migrated");
moveSuccess = YES;
}
else {
FLOG(@"Failed to migrate store: %@", error);
moveSuccess = NO;
}
}
}
}
[newDocument closeWithCompletionHandler:^(BOOL success){
if (!success) {
// Handle the error.
LOG(@" error closing file after creation :-(");
} else {
FLOG(@" file closed");
}
// Async callback to CloudManager to tell it we are done migrating
[self.delegate fileMigrated:sourceDocumentURL success:moveSuccess];
}];
}];
return;
}
/*! Finds the URL for the store file. We have to search for it because there appears to be no way to
get the store file path from a UIManagedDocument API if iCloud options have been used. It's stored somewhere
in ~/DOCUMENTNAME/StoreContent.nosync/CoreDataUbiquitySupport/iCloudAccountID~DeviceID/DOCUMENTUBIQUITYNAME/SomeRandomNumber/store/
WARNING: Its not clear how Core Data stores things if another iCloud account is used. Is a new structure created and are old files removed ?
If not then additional information may be required to identify the correct store to use based on iCloud ID or something.
As an alternative use - (void)storeFileURL2;
@return Returns the URL to the store file.
*/
- (NSURL*)storeFileURL {
FLOG(@"storeFileURL called");
NSString *storeFileName = [OSManagedDocument persistentStoreName];
//FLOG(@" looking for %@", storeFileName);
return [self searchDirectory:self.fileURL for:storeFileName];
}
- (NSURL*)searchDirectory:(NSURL*)directory for:(NSString*)searchFilename {
NSArray *docs = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:directory includingPropertiesForKeys:nil options:0 error:nil];
for (NSURL* document in docs) {
NSString *filename = [document lastPathComponent];
BOOL isDir;
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:document.path isDirectory:&isDir];
if (fileExists && !isDir && [filename isEqualToString:searchFilename]) {
return document;
}
if (fileExists && isDir) {
NSURL *url = [self searchDirectory:document for:searchFilename];
if (url != nil) return url;
}
}
return nil;
}
/*! Gets the URL for the store file. If iCloud is being used then the store file is in some weird
location so this is the safest way to get it. Can only be used once the UIManagedDocument has been
opened!
@return Returns the store file URL
*/
- (NSURL*)storeFileURL2 {
NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
NSArray *stores = [psc persistentStores];
if(stores && stores.count>0) {
NSPersistentStore *store = [[psc persistentStores] objectAtIndex:0];
return store.URL;
} else {
return [self storeFileURL];
}
}
@end
Like this:
Like Loading...