UIManagedDocument & iCloud Integration

Using UIManagedDocument and iCloud together presents some challenges because they don’t play nicely together without some additional work.

One of the main issues that has to be addressed is how to deal with moving documents to and from iCloud in response to the user changing their iCloud preference settings.

Core Data documents don’t really get moved into iCloud, even though we use this terminology. What really happens with an iCloud enabled Core Data document is that change logs get shared via iCloud and peer devices import these change logs into their own local database.  The UIManagedDocument class provides a wrapper that makes creating the Core Data stack easy, but unfortunately UIManagedDocument does not work well with iCloud even though it inherits from UIDocument, which is supposed to work with iCloud.

In this article we will show how one can create a DocumentManager subclass that will make migration to and from iCloud easier.  See the following link for a video demonstrating the iCloud enabled UIManagedDocument in action.

Understanding Core Data and Data Replication

Core Data uses log shipping to replicate database changes between devices.  Each device has it’s own unique database and, when used with the iCloud options, Core Data writes transaction logs each time something gets changed. These transaction logs are replicated to other devices using iCloud’s file replication.

On the receiving device the transaction logs are imported into the local database.  Normal database integrity constraints are applied so it is possible that integrity of the database can be corrupted if the user performs simultaneous transactions on two connected devices.  Ensuring that data integrity is maintain is the responsibility of the application which will need to apply rules that are application for the business domain.

The diagram below shows a typical standalone application that uses Core Data. The application accesses objects in the database via a managedObjectContext.

UIMD_001

When we refer to moving a UIManagedDocument to iCloud we do not mean moving the actual database file to iCloud and sharing it that way, we mean opening with iCloud options such that change logs are written to the shared iCloud container.

We can share the actual database file itself in iCloud but this is not an efficient way to replicate data for most applications because any small change will result in the entire file being replicated, whereas log shipping means only the changes are replicated. However this does mean that each peer needs to have its own copy of the database into which they can import these changes.

Note that for applications that continuously make updates to the same data it might be more efficient to replicate the entire database file rather than replicating the change logs.

UIMD_002

One of the main challenges with the transaction logs synchronisation approach arises when we have an existing database that we want to now share via iCloud.  We can’t just turn on log shipping because peers would then only receive changes from this point – we need some way of generating transaction logs that will allow peers to recreate the entire database from scratch.  With Core Data this is know as migrating the store with iCloud options, and essentially requires building a new local store such that during the building of this store transaction logs are generated in iCloud that allow peer devices to build a copy from scratch too. Core Data proves an API for migrating stores (store is a Core Data term used to describe the actual database file).

To further complicate matters iOS provides UIManagedDocument which is meant to make using Core Data easier, however use of UIManagedDocument with iCloud is poorly documented, and it’s not at all clear when one should be using a native Core Data stack or when one should be using UIManagedDocuments.

Multi-Document vs Library Style Apps

Our aim is to show how it is possible to support both document based and library style applications using UIManagedDocument and iCloud.  Firstly we are going to show how create a document based application and then we will show how to support a library style application using a subset of the functions developed for the document based application.

Each style has a unique set of requirements:

  • Document based applications need mechanisms to detect new files created by peers and files that have been removed by peers.
  • Library style apps needs ways to handle the creation and merging of seed data, particularly when two peers have each built their own copy of the initial database (if you are happy just using the native Core Data stock then see here for a sample iOS library style Core Data app with iCloud integration).

Note the following:

1. We use a file naming convention as follows:

  • local files are named using the user entered FILENAME
  • iCloud files are named using the user entered FILENAME+”_UUID_”+uuid

2. We use the same iCloud filename for the ubiquityNameKey. We also use this naming convention to tell whether a document is shared via iCloud or not.  We can’t tell if a file is in iCloud by looking in iCloud if the user has logged out of iCloud.

3. We are also always using ROLLBACK journal mode because of strange things that have been encountered when using the default WAL mode.

4. We always store documents in the local app /Documents directory. We never put the UIManagedDocument package in the ubiquity container.

5. We support the creation of multiple documents in the app. Apps which only use a single document will probably not need to use the UUID and can use a hardcoded filename and set some additional attributes in the documents metadata to tell if its shared in iCloud or not.

6. We strip off the “_UUID_”+uuid when we display the filename to the user.

7. We scan the ubiquity containers /CoreData subdirectory which contains a subdirectory for each ubiquityNameKey representing each document shared via iCloud. These subdirectories contain the transaction log files used to replicate data.

Implementation

Two classes are primarily used to manage documents under iOS.  OSDocumentManager and OSManagedDocument, a UIManagedDocument subclass.  Two additional classes are involved in the process.  OSDocumentManager initiates the relocation of files in response to changes to the app settings detected when the AppDelegate class triggers a check or when it received a notification that the iCloud account has changed.

OSDocumentManager initiates removal of an iCloud store once it has been migrated to a local store using Core Data APIs, and then removes the local copy of the iCloud store once it detects that Core Data has removed the iCloud data associated with the document. [diagram needs to be updated to reflect recent changes].

UIMD_003

The AppDelegate detects these changes to iCloud settings through one of the following delegate methods:


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;

- (void)applicationWillEnterForeground:(UIApplication *)application;

In practice the settings can only be changed by the user if they switch to the Settings app so we think all cases are covered by the two delegate methods.  But then if the app ran in the background things might be different, or if the device could log the user out of iCloud when they still running the app.

Changes to the iCloud settings triggers the following:

  • iCloud Account is active and Data & Documents is ON
  • Changes to the user preference turning OFF the ‘Use iCloud’ setting prompts the user to indicate whether they want to keep local copies, delete the iCloud files or keep using iCloud.
  • Changes to the user preference turning ‘Use iCloud’ ON does not prompt the user but results in OSDocumentManager automatically migrating local files to iCloud
  • iCloud is turned OFF (log out or turn Data & Documents OFF)
  • The user if given a message indicating that iCloud files will not be accessible and to turn iCloud on if they wish to access the iCloud files again
  • iCloud is turned ON (logged in or turned Data & Documents ON)
  • If the app is first run (user preference has not been set previously) then the user is asked if they would like to Use iCloud and the user preference is saved
  • If the user selects YES then new files are created in iCloud and local copies are created from any existing iCloud files (continuous scan for new or removed iCloud files)

If the user selects to keep the files locally then OSDocumentManager iterates over all the iCloud files, creates an OSManagedDocument for the file and tells it to migrate to a local file, creating the local file this is done synchronously and if it returns successfully OSDocumentManager deletes the iCloud store and the associated UIManagedDocument package.

OSManagedDocument migrates local files to iCloud by first creating a new UIManagedDocument using the local filename convention and then removing the UIManagedDocument’s store from the persistentStoreCoordinator, adding the store from the local file and migrating this store to the same fileURL as the removed store.  Creating the UIManagedDocument is done asynchronously so we have a callback to OSDocumentManager that gets called when the migration is done and the file is closed.  OSDocumentManager then removes the local file.

To create local files from iCloud files is basically exactly the same except we remove all iCloud options when creating the new store. Once the new store is build in the new UIManagedDocument package the OSDocumentManager call back is called and OSDocumentManager then removes the iCloud store and the local UIManagedDocument is removed by the FileListViewController when it detects the removal of the iCloud document.

To complicate matters further we use a File Browser (FileListViewController) that continuously scans for new or removed iCloud files and initiates building or removing local copies.  Because the creation of local and iCloud files does not happen simultaneously we have to block the scans while we are creating or removing files, otherwise it might detect a new file in iCloud and think that because there is not yet a local file it needs to create one.

Library Style Apps
For library style apps that use only a single document the following approach should be used:

  • Use two UIManagedDocuments called DB and DB_ICLOUD (or similar) and migrate between them depending on the users Use iCloud preference setting.
  • In the UIManagedDocument subclass – (void)moveDocumentToICloud method use the following code to create the document name
        // Create the new iCloud filename
        NSString *documentName = [NSString stringWithFormat:@“%@_ICLOUD”,[sourceDocumentURL lastPathComponent]];
  • In the – (void)moveDocumentToLocal method use this line of code
        // Create the new local filename
        NSString *documentName = [[[sourceDocumentURL lastPathComponent] componentsSeparatedByString:@“_ICLOUD”] objectAtIndex:0];

And then change the code in OSDocumentManager to look for a file with or without _ICLOUD in the name and migrate the file if required. Once that’s done set self.managedDocument = DB or DB_ICLOUD depending on the selected preference.

The AppDelegate Methods

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
	    [[OSDocumentManager sharedManager] checkUserICloudPreferenceAndSetupIfNecessary];

}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
	    [[OSDocumentManager sharedManager] performApplicationWillEnterForegroundCheck];

}
- (void)applicationDidEnterBackground:(UIApplication *)application
{

    [[OSDocumentManager sharedManager] saveDocument];

}
- (void)applicationWillTerminate:(UIApplication *)application
{

    [[OSDocumentManager sharedManager] closeDocument];

}

The OSDocumentManager Methods

- (void)checkUserICloudPreferenceAndSetupIfNecessary
{
    // Check preferences and take action as required
}
- (void)performApplicationWillEnterForegroundCheck
{
    // Check preferences and take action as required
}
- (void)promptUserAboutICloudDocumentStorage
{
    // Create alert asking user what action to take
}

- (void)setIsCloudEnabled:(BOOL)isCloudEnabled
{

    [self migrateFilesIfRequired];

}
- (void)migrateFilesIfRequired
{

        // If iCloud has been ENABLED migrate local files to iCloud
        [self migrateLocalFilesToICloud];

        // If iCloud has been DISABLED migrate iCloud files to local
        [self migrateLocalFilesToLocal];

}
- (void)migrateICloudFilesToLocal
{

        // We do them one at a time otherwise the app hangs
        // Once the FileListViewController deletes the local copy
        // migrateFilesIfRequired will get called again to get the next one

        [self migrateICloudFileToLocal:fileURL];

}
- (void)migrateICloudFileToLocal:(NSURL*)fileURL
{

            OSManagedDocument *document = [[OSManagedDocument alloc] initWithFileURL:fileURL];
            document.delegate = self;

            [document moveDocumentToLocal];

}
- (void)migrateLocalFilesToICloud
{

    // App seems to be OK if we just loop through them all without waiting
    for (NSURL *fileURL in docs) {

        [self migrateLocalFileToiCloud:fileURL];

    }

}
- (void)migrateLocalFileToiCloud:(NSURL*)fileURL
{

        OSManagedDocument *document = [[OSManagedDocument alloc] initWithFileURL:fileURL];
        document.delegate = self;
        [document moveDocumentToICloud];

}
- (void)fileMigrated:(NSURL*)migratedURL success:(bool)success
{

        // If it is a local file then delete it
        NSFileManager* fileManager = [[NSFileManager alloc] init];
        NSError *er;
        bool res = [fileManager removeItemAtURL:migratedURL error:&er];
        [self postFileUpdateNotification];

        // If it is an iCloud file then remove the store and iCloud data
        // Our iCloud scanner will then detect this removal and remove the local
        // copy
        [self deleteDocumentAtURL:migratedURL];

}
- (void)deleteDocumentAtURL:(NSURL*)fileURL
{

        // Open UIManagedDocument
        UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:fileURL];

        [doc openWithCompletionHandler:^(BOOL success){
                if (success) {
                	[self removeDocStoresAndClose:doc];
		        }
            }];

}
- (void)removeDocStoresAndClose:(UIManagedDocument *)aDoc
{

        NSURL *storeURL = [[[aDoc managedObjectContext] persistentStoreCoordinator] URLForPersistentStore:store];

        NSError *error;
        NSString *fileName = [[fileURL URLByDeletingPathExtension] lastPathComponent];

        result = [NSPersistentStoreCoordinator removeUbiquitousContentAndPersistentStoreAtURL:storeURL

}
- (void)deleteLocalCopyOfiCloudDocumentAtURL:(NSURL*)fileURL
{
    // When async file delete is completed then call
    [self migrateFilesIfRequired];

}

The UIManagedDocument Methods

- (void)moveDocumentToICloud
{

    // Create a new UIManagedDocument
    OSManagedDocument *newDocument = [[OSManagedDocument alloc] initWithFileURL:destinationURL];

    // Save it and in the completion handler migrate the old store
    [newDocument saveToURL:destinationURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success){

        // Now that we have created a new OSManagedDocument remove the store that was created by UIManagedDocument and migrate the local store to the same location

        NSPersistentStore *store = [[psc persistentStores] objectAtIndex:0];
        // get the store URL
        NSURL *storesURL = store.URL;

        // Now remove the existing store
        [pscForSave removePersistentStore:store error:&removeStoreError];

        // Add the local store
        id sourceStore = [pscForSave addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sourceStoreURL options:localOptions error:nil];

        // Migrate the local store
        id migrationSuccess = [pscForSave migratePersistentStore:sourceStore toURL:storesURL options:ubiquityOptions withType:NSSQLiteStoreType error:&error];

        // Close it and when done tell the delegate you are done migrating
        [newDocument closeWithCompletionHandler:^(BOOL success){

            // Async callback to CloudManager to tell it we are done migrating
            [self.delegate fileMigrated:sourceDocumentURL success:moveSuccess];

        }];

    }];

}
- (void)moveDocumentToLocal
{
    // Create a new UIManagedDocument
    // Now remove the existing store
    // Add the iCloud store
    // Close it and tell the delegate when done

}
- (NSURL*)storeFileURL
{
    // Search for store file by name ‘persistentStore’
}
- (NSURL*)storeFileURL2
{
    // If document is open get the store file URL from the NSPersistentStore itself
}

Code Listings

Related Articles

If there is enough demand I will create sample Library and Document based apps and post the XCode projects. Post a message if you are interested.

19 thoughts on “UIManagedDocument & iCloud Integration

  1. Hi,
    I am lookin at your code in OSManagedDocument. As you saw on SO I am crashing my head about moving from iCloud to local so many thanks for all the help you provided also on the steps before this! I thought it was a matter of a couple of hours to integrate iCloud and it turned out to be something more difficult than writing all the business logic!

    The first thing I am checking is how you move a document from local to iCloud as it is something that I have already done with success and I am surprised.

    Why you do all these steps to move a localonly document to iCloud? I just call the migratePersistentStore:toURL:options:withType:error: passing the right options and under iOS7 it does everything automatically.

    I can see the document that once was local only to start writing logs like Using local storage: 1 and Using local storage: 0. I don’t think there is the need to create anoher UIManagedDocument copy and to delete the old one after migration…

    N

    • This is pretty much how Apple suggested it could be done. Bear in mind my app supports multiple documents and this approach lets me keep track of what’s in ICloud and what’s not in ICloud. Just migrating the store using migratePersistentStore means I could not tell whether the document is in ICloud or not, and in order to open the document you need to know what options to use.

      Not sure how you remove the old stores once you are done but it’s non-trivial because of the asynchronous nature of these methods so you have to use callbacks and completion handlers.

      • I studied and tried your code since yesterday. I started with the method that moves a local only file to iCloud. Unfortunately it doesn’t work in my app. Everything works fine until the existing persistent store is removed then, when a couple of lines later the persistent store is added, the app doesn’t crash but the CPU usage jumps to 99% and keep at this level without a progression to the migration line.

        To be sure it wasn’t a typo I also tried to copy and paste the method from your OSManagedDocument code but it still stops at the same code. I really don’t know why, maybe is something related to the fact I am using a single UIManagedDocument as my app db, it is not a library style app. I also tried to close it and use moveToCloud only after it was closed succesfully.

        This iCloud thing is driving me crazy, it’s three weeks I am on it and the only progress I made are related to the app flow thanks to the advice you gave me on SO. It is exactly as you show in your video now. But still I can’t move my damn single UIManagedDocument to and from iCloud.

  2. great tutorial thank u!
    in this method
    – (void)migrateFilesIfRequired
    {

    // If iCloud has been ENABLED migrate local files to iCloud

    [self migrateLocalFilesToICloud];

    // If iCloud has been DISABLED migrate iCloud files to local

    [self migrateLocalFilesToICloud];

    }
    u would probably need to change the last piece to [self migrateICloudFilesToLocal];

      • could u please post the code for [self postFileUpdateNotification];
        or its just posting notification?

      • It’s just posting a notification for the FileListViewController (an observer) so it knows to rescan and update the file list.

        /*! Posts a notification that files have been changed
        */
        - (void)postFileUpdateNotification {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"iProjectFilesUpdated"
        object:self];
        }

      • and last question 🙂 As i understood we dont keep document packages in icloud, we only keep logs there? then how do i recreate the data on the second device? create new clean managed document and set its persistentstoreoptions’ contentnamekey? and wait for an update? If so, how do i check if there are any logs in icloud?

      • I’ll have to post some of the FileListViewController code which does the metadata query on the ubiquity container and scans for new or deleted files. If a deleted file is detected the delete method in your previous query gets called. If it’s a new file in iCloud then a new file is created with the same UbiquityName. I’ll post it in the morning for you.

      • Thanks, would be great. Ur post helped me out a lot, it covers almost everything i ever wanted to know about UIManagedDocument and iCloud. loading and creating initial documents is the last thing missing 🙂

    • I have just updated to include the FileListViewController and I have restructured the code to remove file and document management code from AppDelegate and from FileListViewController. So pretty much everything is now in OSDocumentManager, but be aware I have not tested the refactored code properly yet.

      • thanks, that makes more sense now:)
        I have this question, what if i have a single UIManagedDocument,
        is it a bad approach to handle it this way:
        1st device:
        1)During the first run in creates the document locally with some default data, after that UbiquitousContenNameKey is set
        2)if user turns off the icloud, we apply just default options to this document without nameKey , if the user turns it on again – we set nameKey again
        so basicly when iCloud is enabled we keep saving logs to iCloud

        2nd device
        1)during the first run we create document, create default data, and set nameKey too. So iCloud should start to import data to this document, and it will save changes from second device too.

        So basicly it should work, or not?

      • You can’t create a document with one set of options and then open it next time with a different set of options.

        Also in the scenario you describe what are you going to do about the duplicate seed data created in both documents?

        I would rather opt for the option of creating the document on the first device with iCloud options on if iCloud is available and then on the second device when running the app first time check if a document exists in iCloud before attempting to create a local document. If one exists then create the local document with iCloud options and don’t add any seed data, let if get that from iCloud.

        In the scenario where both devices documents were created without iCloud then when a device has iCloud turned on check for the existence of an iCloud document and if one exists ask the user which document they want to keep, or whether to merge the data and then take the appropriate action.

  3. Hello ,
    can you please post also an example project for a document based ICloud project please ?
    I’m developing an html editor for IOS and I have big problems with the iCloud integration. It would help me a lot.
    Maybe you could also create a tutorial for ICloud Drive, or I can send you per email an example project made by me with ICloud drive integration and you can help me with my ICloud problem?
    Many thanks

    • Would love to but unless it’s paid work it will have to wait until I have spare time, which is not going to be any time soon. Should be updating apps for iOS8 in the next 6 months though.

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