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
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

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.