iCloud and UIDocument: Beyond the Basics, Part 3/4

This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer. Welcome back to our document-based iCloud app tutorial series! In this tutorial series, we are making a complete document-based iCloud app called PhotoKeeper, with features that go beyond just the basics. In the first and second parts of the […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
Learn how to make a complete UIDocument + iCloud app!

Learn how to make a complete UIDocument + iCloud app!

This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer.

Welcome back to our document-based iCloud app tutorial series!

In this tutorial series, we are making a complete document-based iCloud app called PhotoKeeper, with features that go beyond just the basics.

In the first and second parts of the series, we made a fully-functional, UIDocument-based app that works with local files with full CRUD support.

In this third part of the series, it’s finally time to dive into iCloud! We will get almost everything working on iCloud in this tutorial, except for some subtle bits which we’ll leave for the final part of the series.

This project continues where we left off last time, so if you don’t have it already grab the previous code sample and open up the project. It’s iCloud time, baby!

Checking if iCloud is Available

Before you can use iCloud, you first need to check if it is available by calling NSFileManager’s URLForUbiquityContainerIdentifier method.

This method also initializes iCloud for you, and returns the URL of the “iCloud directory” – that is, the directory that that the iCloud daemon checks for files to sychronize to the cloud.

This call might block so it’s important to call it from a background thread. Let’s try it out. Open PTKMasterViewController.m and make the following changes:

// Add new private instance variable
NSURL * _iCloudRoot;
BOOL _iCloudAvailable;

// Add to end of "Helpers" section
- (void)initializeiCloudAccessWithCompletion:(void (^)(BOOL available)) completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        _iCloudRoot = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
        if (_iCloudRoot != nil) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"iCloud available at: %@", _iCloudRoot);
                completion(TRUE);
            });            
        }            
        else {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"iCloud not available");
                completion(FALSE);
            });
        }
    });
}

The most important part here is that we call URLForUbiquityContainerIdentifier to figure out where the iCloud root directory is. If it returns nil, that means iCloud isn’t available.

Notice we take a block as a parameter – it’s a method we’ll call after URLForUbiquityContainerIdentifier completes to indicate whether or not iCloud is available. If you’re a bit rusty on blocks, you might want to check out our blocks tutorial.

Now let’s try this out. Go to the refresh method and replace it with the following:

- (void)refresh {
    
    [_objects removeAllObjects];
    [self.tableView reloadData];
    
    self.navigationItem.rightBarButtonItem.enabled = NO;
    [self initializeiCloudAccessWithCompletion:^(BOOL available) {
        
        _iCloudAvailable = available;

        // TODO

        if (![self iCloudOn]) {
            [self loadLocal];        
        }
        
    }];
}

The only difference here is we wait until we check if iCloud is available before refreshing our list of files.

Compile and run the app on your device – not the simulator, because iCloud doesn’t work on the simulator. You should see this in the console:

iCloud not available

You should see this whether you have iCloud enabled or not in Settings. What gives?

This is because for your app to use iCloud, you have to do several project configuration steps. Let’s get to it!

Configuring your Project for iCloud

The first step is to visit the iOS Developer Center and go to the iOS Provisioning Portal. Click App IDs in the sidebar and then New App ID. Create an App ID for your app, similar to the following:

Creating an App ID

After you are done, you will see your App ID in the list:

Configuring an App ID for iCloud

Note that iCloud is not enabled by default – you have to configure it. To do so, simply click the Configure button, check the checkbox for Enable for iCloud, and click Done.

Enabling an App ID for iCloud

Next you need to create a provisioning profile for your App ID. Click the Provisioning tab, click New Profile, and select the appropriate information like you can see below.

Creating a Provisioning Profile

After you finish creating the profile, refresh the page until it is available for download, and download it. Double click it to install it in Xcode. You should see it in the Organizer when you’re done:

Viewing a Provisioning Profile in Organizer

Next, you need to set up your Xcode project to use this provisioning profile. Click your project in the Project Navigator, select the PhotoKeeper target, adn go to the Build Settings tab. Search for code sign, and set the code signing identity to your new provisioning profile.

Using Provisioning Profile

To use iCloud, you need to set up some Entitlements that gives your app access to the iCloud directory. This used to be a pain but now it’s extremely simple. Just go to the Summary tab and click the checkbox for Enable Entitlements. It should autofill everything else, but sometimes I’ve seen it not fill out the iCloud Containers section, so you might have to click the plus button and add it if so.

Enabling iCloud Entitlements

The last step is you need to configure your app to recognize the file types for the documents the app is storing. This isn’t absolutely required at this point, but you will run into troubles if you don’t do it later so you might as well do it now.

Switch to the Info tab and click the plus button in the bottom right. In the pop-up, select Add Document Type.

Adding a Document Type

You will see that it has created a new entry for you in the Document Types list above. Fill it out similarly to this example:

Configuring a Document Type

Next click the plus button again but select Add Exported UTI this time.

Adding an Exported UTI

It will create a new entry for you in the Exported UTIs list above. Fill it out similarly to this example:

Configuring an Exported UTI

It is important that whatever you put for Identifier here matches what you put for Types in the Document Types. Also, I have had trouble with filename extensions that weren’t exactly 3 characters in length, so if you have any troubles in your app try working with 3 character extensions.

Phew – finally done! Compile and run your app, and if all works well you should now see something like this in the console:

iCloud available at: file://localhost/private/var/mobile/Library/Mobile%20Documents/
KFCNEC27GU~com~razeware~PhotoKeeper/

This gives you the directory on the device where you can find and create iCloud files. The iCloud daemon will automatically pull down new files to this directory as they become available, update files that are there, watch for changes you put there, etc.

We’ll use this directory a lot in this tutorial, but before we go we have the first tricky bit about iCloud to discuss – turning iCloud on and off.

iCloud On, iCloud Off

Like we mentioned back in part 1, you shouldn’t just assume you should use iCloud if it’s available – you should allow the user to enable/disable iCloud for your app.

And also like we discussed before, I believe the best place for this configuration option is in Settings (rather than in-app settings). This way it reduces clutter in your app and discourages users from changing it unnecessarily.

Adding a switch to turn iCloud on and off is easy, so let’s deal with that first. Create a new file with the iOS\Resource\Settings Bundle template, and name it Settings.bundle. Open Settings.bundle\Root.plist, and modify the file to look like the following:

Configuring the Settings.bundle plist to add a switch

Compile and run the app on your device, and switch to the Settings app. Scroll down to find the category for PhotoKeeper, and you should see a switch for iCloud:

iCloud On/Off Switch in Settings

Settings is pretty cool because getting the value for this switch is as easy as reading the “iCloudOn” key from NSUserDefaults.

Now that we’ve got a switch, it starts getting tricky. We have to make sure we have code that handles all of the following cases:

  • If iCloud isn’t switched on, but it’s available (and we haven’t bugged the user already), ask the user if they want to turn on iCloud. This way they don’t necessarily have to go to Settings at all.
  • If iCloud isn’t available, but it was on before, warn the user that although the app can’t access their iCloud files anymore, they’re still out on the cloud.
  • If iCloud was switched on (but wasn’t previously), that means we need to move our local files to iCloud.
  • If iCloud was switched off (but wasn’t previously), that means we need to ask the user what to do with the old iCloud files. The user might want to leave them on iCloud, keep a copy, or change their mind and turn iCloud back on again.

To accomplish all this, in addition to the “iCloudOn” flag managed by Settings, we need to create two more NSUserDefaults flags – one for if iCloud “used to be on”, and one for if we’ve prompted the user if they want to use iCloud yet.

Add these wrappers to PTKMasterViewController.m at the top of the “Helpers” section (overwriting the old iCloudOn method):

- (BOOL)iCloudOn {    
    return [[NSUserDefaults standardUserDefaults] boolForKey:@"iCloudOn"];
}

- (void)setiCloudOn:(BOOL)on {    
    [[NSUserDefaults standardUserDefaults] setBool:on forKey:@"iCloudOn"];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

- (BOOL)iCloudWasOn {    
    return [[NSUserDefaults standardUserDefaults] boolForKey:@"iCloudWasOn"];
}

- (void)setiCloudWasOn:(BOOL)on {    
    [[NSUserDefaults standardUserDefaults] setBool:on forKey:@"iCloudWasOn"];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

- (BOOL)iCloudPrompted {
    return [[NSUserDefaults standardUserDefaults] boolForKey:@"iCloudPrompted"];
}

- (void)setiCloudPrompted:(BOOL)prompted {    
    [[NSUserDefaults standardUserDefaults] setBool:prompted forKey:@"iCloudPrompted"];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

Then make the following changes to PTKMasterViewController.m:

// Add some stub methods to the bottom of the "File management" section
- (void)iCloudToLocal {
    NSLog(@"iCloud => local");
}

- (void)localToiCloud {
    NSLog(@"local => iCloud");
}

#pragma mark iCloud Query

- (void)startQuery {        
    NSLog(@"Starting to watch iCloud dir...");
}

// Replace the "TODO" in refresh with the following
if (!_iCloudAvailable) {
    
    // If iCloud isn't available, set promoted to no (so we can ask them next time it becomes available)
    [self setiCloudPrompted:NO];
    
    // If iCloud was toggled on previously, warn user that the docs will be loaded locally
    if ([self iCloudWasOn]) {
        UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"You're Not Using iCloud" message:@"Your documents were removed from this iPad but remain stored in iCloud." delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
        [alertView show];
    }
    
    // No matter what, iCloud isn't available so switch it to off.
    [self setiCloudOn:NO]; 
    [self setiCloudWasOn:NO];
    
} else {        
    
    // Ask user if want to turn on iCloud if it's available and we haven't asked already
    if (![self iCloudOn] && ![self iCloudPrompted]) {
        
        [self setiCloudPrompted:YES];
        
        UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"iCloud is Available" message:@"Automatically store your documents in the cloud to keep them up-to-date across all your devices and the web." delegate:self cancelButtonTitle:@"Later" otherButtonTitles:@"Use iCloud", nil];
        alertView.tag = 1;
        [alertView show];
        
    } 
    
    // If iCloud newly switched off, move local docs to iCloud
    if ([self iCloudOn] && ![self iCloudWasOn]) {                    
        [self localToiCloud];                                                           
    }                
    
    // If iCloud newly switched on, move iCloud docs to local
    if (![self iCloudOn] && [self iCloudWasOn]) {
        [self iCloudToLocal];                    
    }
    
    // Start querying iCloud for files, whether on or off
    [self startQuery];
    
    // No matter what, refresh with current value of iCloudOn
    [self setiCloudWasOn:[self iCloudOn]];
    
}

// Add right after the refresh method
#pragma mark UIAlertViewDelegate

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
    
    // @"Automatically store your documents in the cloud to keep them up-to-date across all your devices and the web."
    // Cancel: @"Later"
    // Other: @"Use iCloud"
    if (alertView.tag == 1) {
        if (buttonIndex == alertView.firstOtherButtonIndex) 
        {
            [self setiCloudOn:YES];            
            [self refresh];
        }                
    } 
}

There’s a lot of code here, but none of it is very complicated – it’s just implementing the logic discussed earlier. Be sure to read through the refresh method to make sure you understand the basic logic there.

You may wonder why we’re calling “startQuery” even if iCloud is off. We’re going to need valid a list of iCloud files for when we implement the iCloudToLocal method. But don’t worry, we won’t display the list if iCloud is off. More on this later.

There’s one final step. Since the user can leave the app and change the value in Settings at any time, we need to refresh whenever the user returns to the app (i.e. the app “didbecomeactive”). So make the following changes to listen for that event:

// Add to the bottom of viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];

// Add right after viewDidLoad
- (void)didBecomeActive:(NSNotification *)notification {    
    [self refresh];
}

Run the app and perform the following steps:

  • Make sure iCloud is off and create a local file. Switch to settings and turn iCloud on. You should no longer see the file.
  • Make sure iCloud is on and go to Settings\iCloud\Documents and Data and switch it off. This effectively makes iCloud not available. Switch back to the app and you should get a popup, and it should switch to iCloud off automatically.
  • Switch back to Settings and make iCloud available again. Switch back to the app and it will tell you iCloud is available and ask if you want to turn it on.
  • Play around with switching iCoud on and off, ana make sure the appropriate “local => iCloud” or “iCloud => local” messages print out.

Nice! Now that we have this firm framework in place, all we really need to care about from here on out is whether “iCloudOn” is YES or NO. If YES, we should be displaying iCloud files, otherwise local files.

But we haven’t covered yet how to get a list of iCloud files. Let’s cover that now!

Querying iCloud Files

As we mentioned in part 1, you can’t just enumerate the iCloud directory with NSFileManager APIs like you do for the documents directory. Instead, you have to use a fancy API called NSMetadataQuery.

This API lets you set up a search on a directory, such as “give me all files that end with PTK”. It will return the initial results, and then continue to watch the directory for you, sending you update notifications as they come in. It’s pretty handy and efficient in comparison to the alternative (periodic polling).

Let’s see what it looks like. Go to the “iCloud Query” section and add this right above startQuery:

- (NSMetadataQuery *)documentQuery {
    
    NSMetadataQuery * query = [[NSMetadataQuery alloc] init];
    if (query) {
        
        // Search documents subdir only
        [query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
        
        // Add a predicate for finding the documents
        NSString * filePattern = [NSString stringWithFormat:@"*.%@", PTK_EXTENSION];
        [query setPredicate:[NSPredicate predicateWithFormat:@"%K LIKE %@",
                             NSMetadataItemFSNameKey, filePattern]];        
        
    }
    return query;
    
}

When you create a NSMetadataQuery, you can tell it to watch the “Documents” subdirectory of the iCloud root (this is where we’ll be storing our files) by passing in NSMetadataQueryUbiquitousDocumentsScope as the search scope.

You can then filter the output with a predicate. We set up one here to look for files that end with the PTK extension.

Next let’s create some methods to start and stop this query. Make the following changes to PTKMasterViewController.m:

// Add new private instance variables
NSMetadataQuery * _query;
BOOL _iCloudURLsReady;
NSMutableArray * _iCloudURLs;

// Add to "iCloud query" section after documentQuery method, replacing the existing startQuery method
- (void)stopQuery {
    
    if (_query) {
        
        NSLog(@"No longer watching iCloud dir...");
        
        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:nil];
        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidUpdateNotification object:nil];
        [_query stopQuery];
        _query = nil;
    }
    
}

- (void)startQuery {
    
    [self stopQuery];
    
    NSLog(@"Starting to watch iCloud dir...");
    
    _query = [self documentQuery];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(processiCloudFiles:)
                                                 name:NSMetadataQueryDidFinishGatheringNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(processiCloudFiles:)
                                                 name:NSMetadataQueryDidUpdateNotification
                                               object:nil];
    
    [_query startQuery];
}

// Add in viewDidLoad before call to refresh
_iCloudURLs = [[NSMutableArray alloc] init];

// Add at beginning of refresh method
_iCloudURLsReady = NO;
[_iCloudURLs removeAllObjects];

startQuery gets the query and starts it up. It also registers for two notifications – the initial gather notification, as well as further updates. In both cases, processiCloudFiles will be called – we’ll write that next. stopQuery just does the opposite and tears things down.

Continue by adding the implementation of processiCloudFiles:

- (void)processiCloudFiles:(NSNotification *)notification {
    
    // Always disable updates while processing results
    [_query disableUpdates];
    
    [_iCloudURLs removeAllObjects];
    
    // The query reports all files found, every time.
    NSArray * queryResults = [_query results];
    for (NSMetadataItem * result in queryResults) {
        NSURL * fileURL = [result valueForAttribute:NSMetadataItemURLKey];
        NSNumber * aBool = nil;
        
        // Don't include hidden files
        [fileURL getResourceValue:&aBool forKey:NSURLIsHiddenKey error:nil];
        if (aBool && ![aBool boolValue]) {
            [_iCloudURLs addObject:fileURL];
        }        
        
    }        
    
    NSLog(@"Found %d iCloud files.", _iCloudURLs.count);
    _iCloudURLsReady = YES;
    
    if ([self iCloudOn]) {
        
        // Remove deleted files
        // Iterate backwards because we need to remove items form the array
        for (int i = _objects.count -1; i >= 0; --i) {
            PTKEntry * entry = [_objects objectAtIndex:i];
            if (![_iCloudURLs containsObject:entry.fileURL]) {
                [self removeEntryWithURL:entry.fileURL];
            }
        }
        
        // Add new files
        for (NSURL * fileURL in _iCloudURLs) {                
            [self loadDocAtURL:fileURL];        
        }
        
        self.navigationItem.rightBarButtonItem.enabled = YES;
        
    } 
        
    [_query enableUpdates];
    
}

When you’re processing the results of an NSMetadataQuery, it’s important to stop the query so you don’t get more updates while you’re in the middle of processing the first, so we do that at the beginning and end of the method.

We then loop through the query results, and add everything to the list except for items that are hidden. We add all of the results to the most up-to-date list of _iCloudURLs, which we will find handy later, and set a flag indicating that they are ready.

If iCloud isn’t on, that’s all we do. But if it is on, we make sure our local list of entries matches the list of iCloud files. We first loop through our entries and remove anything not in the list of iCloud files by calling removeEntryWithURL. We also add anything that isn’t already in the list to the list by calling loadDocAtURL. When this is all done it’s safe to add a new document again, so we enable the right bar button item.

Next, add these methods right after viewDidUnload:

- (void)viewWillAppear:(BOOL)animated {
    [_query enableUpdates];
}

- (void)viewWillDisappear:(BOOL)animated {
    [_query disableUpdates];
}

This is important to add because UIDocument has issues if you try to open more than instance at a time pointing to the same fileURL, so we don’t want our table view trying to refresh itself (hence loading a UIDocument to get its medatata) while we’re editing a document.

We could run the app now but there wouldn’t be any use, as there’s no way to create a file in iCloud yet. Luckily, since we already laid most of the groundwork by using UIDocument this is quite easy to fix! Simply replace getDocURL with the following:

- (NSURL *)getDocURL:(NSString *)filename {    
    if ([self iCloudOn]) {
        NSURL * docsDir = [_iCloudRoot URLByAppendingPathComponent:@"Documents" isDirectory:YES];
        return [docsDir URLByAppendingPathComponent:filename];
    } else {
        return [self.localRoot URLByAppendingPathComponent:filename];    
    }
}

Here we’ve implemented the “iCloudOn” case to return the Documents subdirectory of the iCloud root, where we will be saving our files.

When we go to create a new file, now it will be creating it in the iCloud directory, so the daemon will pick up on it and synchronize it. Everything else will just work since we’re using UIDocument!

Compile and run on your device, and make sure iCloud is available and switched on. Create some documents and verify that they are created in the iCloud directory by looking at the logs.

Now for the fun part – start up a second device and you should see the same files pull down from iCloud! :D

iCloud Synchronizing across 2 Devices

If you have any troubles, try deleting the app from your device and re-installing, double checking that your Document exports are set up properly, or compare your project to the solution at the end of this tutorial.

Fixing Deleting

If you try deleting a file right now, it may work, but it’s currently doing things in an unsafe way according to the Document-Based App Programming Guide.

When you’re dealing with files in an iCloud directory, you can’t just use NSFileManager APIs directly like you could with local files. Instead, you have to use the NSFileCoordinator class to make sure it’s safe to modify the files. This is important since the iCloud daemon is also working with this directory at the same time.

To see what this looks like, open PTKMasterViewController.m and replace deleteEntry with the following:

- (void)deleteEntry:(PTKEntry *)entry {
    
    // Wrap in file coordinator
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
        NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
        [fileCoordinator coordinateWritingItemAtURL:entry.fileURL 
                                            options:NSFileCoordinatorWritingForDeleting
                                              error:nil 
                                         byAccessor:^(NSURL* writingURL) {                                                   
                                             // Simple delete to start
                                             NSFileManager* fileManager = [[NSFileManager alloc] init];
                                             [fileManager removeItemAtURL:entry.fileURL error:nil];
                                         }];
    });    
    
    // Fixup view
    [self removeEntryWithURL:entry.fileURL];
    
}

- 

Here we create a NSFileCoordinator and use the coordinateWritingAtURL method passing in NSFileCoordinatorWritingForDeletign to make sure we have access to delete the file. This method may block, so it’s important to run it on a background thread. Once we have access, it’s safe to delete it as usual.

Compile and run, and now you should be able to delete entries – but safely this time!

Fixing Renaming

Similarly, we need to fix up renaming. In the renameEntry method, replace the “Simple renaming to start” section with the following:

// Wrap in file coordinator
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
    NSError * error;
    NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
    [coordinator coordinateWritingItemAtURL:entry.fileURL options: NSFileCoordinatorWritingForMoving writingItemAtURL:newDocURL options: NSFileCoordinatorWritingForReplacing error:&error byAccessor:^(NSURL *newURL1, NSURL *newURL2) {
        
        // Simple renaming to start
        NSFileManager* fileManager = [[NSFileManager alloc] init];
        NSError * error;
        BOOL success = [fileManager moveItemAtURL:entry.fileURL toURL:newDocURL error:&error];
        if (!success) {
            NSLog(@"Failed to move file: %@", error.localizedDescription);
            return;
        }
        
    }];
});

This is exactly the same as we did for deleting files, except we call a slightly different method on NSFileCoordinator with parameters for moving a file.

Compile and run, and try to create a file, and… wait, it doesn’t work! The console shows the following:

Failed to move file: The operation couldn’t be completed. (Cocoa error 4.)

To be honest, I have no idea why we get this error – it seems to me that this should work. If anyone knows (maybe I have passed the wrong parameters to coordinateWritingItemAtURL:entry.fileURL?) let me know.

Update: Jim Tobin wrote in to let me know that he got this to work by creating a copy of entry.fileURL and passing that in instead. I haven’t had a chance to verify this yet, but feel free to give it a try!

In the meantime, I have another method that works (although is slightly hackish) – save the old file to a new filename, and delete the old file. Try it out by replacing the above code block with the following:

// Rename by saving/deleting - hack?
NSURL * origURL = entry.fileURL;
UIDocument * doc = [[PTKDocument alloc] initWithFileURL:entry.fileURL];
[doc openWithCompletionHandler:^(BOOL success) {
    [doc saveToURL:newDocURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
        NSLog(@"Doc saved to %@", newDocURL);                        
        [doc closeWithCompletionHandler:^(BOOL success) {
            
            // Update version of file
            dispatch_async(dispatch_get_main_queue(), ^{
                entry.version = [NSFileVersion currentVersionOfItemAtURL:newDocURL];
                int index = [self indexOfEntryWithFileURL:entry.fileURL];
                [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:index inSection:0]] withRowAnimation:UITableViewRowAnimationNone];    
            });                
            
            // Delete old file
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
                NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
                [fileCoordinator coordinateWritingItemAtURL:origURL 
                                                    options:NSFileCoordinatorWritingForDeleting
                                                      error:nil 
                                                 byAccessor:^(NSURL* writingURL) {                                                   
                                                     NSFileManager* fileManager = [[NSFileManager alloc] init];
                                                     [fileManager removeItemAtURL:writingURL error:nil];
                                                 }];
            });
            NSLog(@"Doc deleted at %@", origURL);
        }];
    }];
}];  

Compile and run, and you should now be able to rename files at will!

Note that the way the app is currently designed, when you rename a file it will disappear temporarily (because the old file was deleted) and reappear a second later (as the new file is noticed and opened). This is because our table view is refreshed upon the results of the NSMetadataQuery. If anyone has a better strategy to deal with updates to avoid this problem, please drop a note in the forum discussion!

Detecting Updates

The last thing we’ll cover in this part of the tutorial is detecting updates to an opened document. Right now, if you open the same document on two devices and change the photo on one of the devies, the other device won’t notice that it’s changed.

Luckily, UIDocument has built-in support to notice changes and update itself behind the scenes! All we need to do is register for a notification when this occurs so we can update the detail view controller.

Open PTKDetailViewController.m and add these methods right after shouldAutorotateToInterfaceOrientation:

- (void)viewWillAppear:(BOOL)animated {
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(documentStateChanged:)
                                                 name:UIDocumentStateChangedNotification 
                                               object:self.doc];
        
}

- (void)viewWillDisappear:(BOOL)animated {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)documentStateChanged:(NSNotification *)notificaiton {
    
    [self configureView];
    
}

Here we just wait for the UIDocumentStateChangedNotification notification and refresh the view if it occurs. Pretty simple, eh?

Compile and run on two devices, and open the same document on two devices. Now when you modify the document on device 1, the second device will notice and update as well!

Where To Go From Here?

Here is a download with the state of the code sample so far.

At this point, this app is fully functional for local documents, mostly functional for iCloud documents (with full create, read, update, delete support), and allows you to toggle back and forth between the two options in Settings.

However, there are still some subtle aspects remaining that we need to address. We need to implement those methods to move documents to and from iCloud when the user switches iCloud on or off, and we need to deal with the dreaded iCloud conflicts!

So when you’re ready, move on to the final part of this tutorial series, where we will cover those topics. In the meantime, if you have any comments, questions, or suggestions for improvement, please join the forum discussion below!


This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer.

Contributors

Over 300 content creators. Join our team.