UIManagedDocument Subclass Implementation

@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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s