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

This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer. Welcome back to the final part of 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 […] 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 the final part of 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, we got iCloud mostly working. All CRUD operations are functional, and you can even rename and delete files.

That brings us to this fourth and final part of the series! Here we’ll show you how you can resolve conflicts, and move/copy files to/from iCloud.

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. Let’s wrap this project up!

iCloud and Conflicts: Overview

Open your project and open the same document on two different devices. Change the picture on both at the same time, tap Done, and see what happens.

Basically iCloud will automatically choose the “lastest change” as the current document by default. But since this is dependent on network connections, this might not be what the user actually wants.

The good news is when the same document is modified at the same time like this, iCloud will detect this as a conflict and store copies of any conflicting documents. It’s up to you how you deal with the conflict – you could try to merge changes or let the user pick one (or more) of the versions to keep.

In this tutorial, when there’s a conflict we’re going to display a view controller to the user that shows the conflicts and allows the user to pick one of them to keep. You might want to use a slightly different strategy for your app, but the same core idea remains.

Displaying Conflicts

The first thing we need to do is display and detect conflicts. UIDocument has a property called documentState that is a bitfield of different flags that can be on a document, including a “conflict” state.

We’ve actually been passing this along in our PTKEntry all along, now’s our chance to actually use it!

Let’s start by just logging out what the document state is when we read documents. Make the following changes to PTKMasterViewController.m:

// Add to top of "Helpers" section
- (NSString *)stringForState:(UIDocumentState)state {
    NSMutableArray * states = [NSMutableArray array];
    if (state == 0) {
        [states addObject:@"Normal"];
    }
    if (state & UIDocumentStateClosed) {
        [states addObject:@"Closed"];
    }
    if (state & UIDocumentStateInConflict) {
        [states addObject:@"In Conflict"];
    }
    if (state & UIDocumentStateSavingError) {
        [states addObject:@"Saving error"];
    }
    if (state & UIDocumentStateEditingDisabled) {
        [states addObject:@"Editing disabled"];
    }
    return [states componentsJoinedByString:@", "];
}

// Replace "Loaded File URL" log line in loadDocURL with the following
NSLog(@"Loaded File URL: %@, State: %@, Last Modified: %@", [doc.fileURL lastPathComponent], [self stringForState:state], version.modificationDate.mediumString);

Compile and run your app, and if you have any documents in the conflict state you should see something like this:

Loaded File URL: Photo 4.ptk, State: In Conflict, Last Modified: Today 10:01:17 AM

Let’s modify our app to have a visual representation of when a document is in the conflict state. Open MainStoryboard.storyboard and add a 32×32 image view to the bottom left of the thumbnail image view, and set the image to warning.png:

Adding warning image to table view cell

Then switch to PTKEntryCell.h and add a property for this:

@property (weak, nonatomic) IBOutlet UIImageView * warningImageView;

And synthesize it in PTKEntryCell.m:

@synthesize warningImageView;

Switch back to MainStoryboard.storyboard, control-drag from the cell to the warning image view, and connect it to the warningImageView outlet.

Finally, open PTKMasterViewController.m and add this to the end of tableView:cellForRowAtIndexPath (before the call to return cell):

if (entry.state & UIDocumentStateInConflict) {
    cell.warningImageView.hidden = NO;
} else {
    cell.warningImageView.hidden = YES;
}

Compile and run, and now if there is a conflict you should see a warning flag next to the document in question!

Seeing when a UIDocument is in conflict in the table view

Creating a Conflict View Controller

Now that we know what documents are in conflict, we have to do something about it. We’ll create a view controller that lists all the versions that are available and let the user choose one to keep.

Create a new file with the iOS\Cocoa Touch\Objective-C class template, name it PTKConflictViewController, and make it a subclass of UITableViewController. Make sure the checkboxes are NOT checked, and finish creating the file.

We’ll add the code for this later, first let’s design the UI. Open MainStoryboard.storyboard, and drag a new UITableViewController into the storyboard below the detail view controller.

Adding a new table view controller to the Storyboard editor

We want the master view controller to display it, so control-drag from the master view controller to the new table view controller, and choose Push from the popup. Then click the segue and name it “showConflicts”:

Adding a new segue to display the conflicts view controller

Next zoom in to 100% size and let’s set up the table view cell. Set the row height to 80, and add an image view and two labels roughly like the following:

Laying out a table view cell for conflicts

We could create a custom cell sublass like we did before for the master table view, but I’m feeling lazy so we’ll look up the subviews by tag instead. So set the tag of the image view to 1, the first label to 2, and the second label to 3.

Setting the tag on a view in the Storyboard editor

To finish configuring the cell we need to set a cell reuse identifier for our cell so we can dequeue it. Select the Table View Cell and set the Identifeir to MyCell.

Setting the conflict cell's identifier

Finally, we need to set the class of our view controller. Select the table View Controller and in the identity inspector set the cass to PTKConflictViewController.

Setting the view controller class in the Identity inspector

Before we implement the PTKConflictViewController, we should create a class to keep track of everything we need to know for each cell (similar to PTKEntry).

For a conflicted file, you can get a list of all of the different versions of the file that are in conflict. These versions are referenced via the NSFileVersion class.

So we need to keep track of the NSFileVersion for each row, open that version and get its metadata as well (so we can display the thumbnail to the user).

Create a new file with the iOS\Cocoa Touch\Objective-C class template, name it PTKConflictEntry, and make it a subclass of NSObject. Then replace the contents of PTKConflictEntry.h with the following:

#import <Foundation/Foundation.h>

@class PTKMetadata;

@interface PTKConflictEntry : NSObject

@property (strong) NSFileVersion * version;
@property (strong) PTKMetadata * metadata;

- (id)initWithFileVersion:(NSFileVersion *)version metadata:(PTKMetadata *)metadata;

@end

And replace PTKConflictEntry.m with the following:

#import "PTKConflictEntry.h"

@implementation PTKConflictEntry
@synthesize version = _version;
@synthesize metadata = _metadata;

- (id)initWithFileVersion:(NSFileVersion *)version metadata:(PTKMetadata *)metadata {
    if ((self = [super init])) {
        self.version = version;
        self.metadata = metadata;
    }
    return self;
}

@end

Finally onwards to implementing PTKConflictViewController! When displaying the view controller, we’ll pass in the fileURL to display conflicts for, so modify PTKConflictViewController.h to the following:

#import <UIKit/UIKit.h>

@interface PTKConflictViewController : UITableViewController

@property (strong) NSURL * fileURL;

@end

Then make the following changes to PTKConflictViewController.m:

// Add imports to the top of the file
#import "PTKConflictEntry.h"
#import "PTKDocument.h"
#import "PTKMetadata.h"
#import "NSDate+FormattedStrings.h"

// Modify @implementation to add a private instance variable for the entries and synthesize fileURL
@implementation PTKConflictViewController {
    NSMutableArray * _entries;
}
@synthesize fileURL;

// Add to end of viewDidLoad
_entries = [NSMutableArray array];

// Replace numberOfSectionsInTableView, numberOfRowsInSection, and cellForRowAtIndexPath with the following
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSLog(@"%d rows", _entries.count);
    return _entries.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"MyCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    
    // Configure the cell...
    UIImageView * imageView = (UIImageView *) [cell viewWithTag:1];
    UILabel * titleLabel = (UILabel *) [cell viewWithTag:2];
    UILabel * subtitleLabel = (UILabel *) [cell viewWithTag:3];
    PTKConflictEntry * entry = [_entries objectAtIndex:indexPath.row];
    
    if (entry.metadata) {
        imageView.image = entry.metadata.thumbnail;
    }
    titleLabel.text = [NSString stringWithFormat:@"Modified on %@", entry.version.localizedNameOfSavingComputer];
    subtitleLabel.text = [entry.version.modificationDate mediumString];
    
    return cell;
}

OK so that’s pretty straightforward – we’re just displaying any PTKConflictEntry classes in the _entries array.

But what about creating these PTKConflictEntry classes in the first place from the fileURL? We’ll do that in viewWillAppear. Add this right after viewDidUnload:

- (void)viewWillAppear:(BOOL)animated {
    
    [_entries removeAllObjects];    
    NSMutableArray * fileVersions = [NSMutableArray array];
    
    NSFileVersion * currentVersion = [NSFileVersion currentVersionOfItemAtURL:self.fileURL];
    [fileVersions addObject:currentVersion];
    
    NSArray * otherVersions = [NSFileVersion otherVersionsOfItemAtURL:self.fileURL];
    [fileVersions addObjectsFromArray:otherVersions];
    
    for (NSFileVersion * fileVersion in fileVersions) {
        
        // Create a resolve entry and add to entries
        PTKConflictEntry * entry = [[PTKConflictEntry alloc] initWithFileVersion:fileVersion metadata:nil];
        [_entries addObject:entry];
        NSIndexPath * indexPath = [NSIndexPath indexPathForRow:_entries.count-1 inSection:0];
        
        // Open doc and get metadata - when done, reload row so we can get thumbnail
        PTKDocument * doc = [[PTKDocument alloc] initWithFileURL:fileVersion.URL];
        NSLog(@"Opening URL: %@", fileVersion.URL);
        [doc openWithCompletionHandler:^(BOOL success) {
            if (success) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    entry.metadata = doc.metadata;
                    [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
                });
                [doc closeWithCompletionHandler:nil];
            }
        }];
    }
    
    [self.tableView reloadData];
}

To get the different versions of the file that are in conflict, you use two different APIs in NSFileVersion – currentVersionOfItemAtURL (the current “winning” version), and otherVersionsOFItemAtURL (the other ones that iCloud isn’t sure about). We open up all of these versions and add them to our array, along with their metadata (thumbnail).

Finally, when the user taps a row we want to choose that file version and resolve the conflict. So replace didSelectRowAtIndexPath at the end of the file with this:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    PTKConflictEntry * entry = [_entries objectAtIndex:indexPath.row];
    
    if (![entry.version isEqual:[NSFileVersion currentVersionOfItemAtURL:self.fileURL]]) {
        [entry.version replaceItemAtURL:self.fileURL options:0 error:nil];    
    }
    [NSFileVersion removeOtherVersionsOfItemAtURL:self.fileURL error:nil];
    NSArray* conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:self.fileURL];
    for (NSFileVersion* fileVersion in conflictVersions) {
        fileVersion.resolved = YES;
    }
    
    [self.navigationController popViewControllerAnimated:YES];    
    
}

If the user chose one of the “other” versions of the file, we replace the current version of the file with what they chose.

We then remove all the other versions, and mark everything as resolved. Not too hard, eh?

Almost ready to try this out! Just make the following changes to PTKMasterViewController.m:

// Add new private variable
NSURL * _selURL;

// Modify didSelectRowAtIndexPath to check documenet state first
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    PTKEntry * entry = [_objects objectAtIndex:indexPath.row];
    [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    if (entry.state & UIDocumentStateInConflict) {
        
        _selURL = entry.fileURL;
        [self performSegueWithIdentifier:@"showConflicts" sender:self];
        
    } else {
        
        _selDocument = [[PTKDocument alloc] initWithFileURL:entry.fileURL];    
        [_selDocument openWithCompletionHandler:^(BOOL success) {
            NSLog(@"Selected doc with state: %@", [self stringForState:_selDocument.documentState]);
            
            dispatch_async(dispatch_get_main_queue(), ^{
                [self performSegueWithIdentifier:@"showDetail" sender:self];
            });
        }];
        
    }
} 

// Modify prepareForSegue to add a case for the showConflicts segue
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showDetail"]) {
        [[segue destinationViewController] setDelegate:self];
        [[segue destinationViewController] setDoc:_selDocument];
    } else if ([[segue identifier] isEqualToString:@"showConflicts"]) {
        [[segue destinationViewController] setFileURL:_selURL];
    }
}

And that’s it! Compile and run, and now when you tap an entry with a conflict you will now see the list of versions in conflict:

The conflict view controller in PhotoKeeper

And you can tap a file to restore to that verison and resolve the conflict:

A resolved conflict in PhotoKeeper

Contributors

Over 300 content creators. Join our team.