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

Ray Wenderlich
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

Moving Files To iCloud

At this point our app is almost done, except for one more bit – dealing with the user transitioning between iCloud off and iCloud on, and vice versa. There are two cases:

  • local => iCloud. If the user isn’t using iCloud (and has local files) and starts using iCloud, the app should automatically move all the local files to iCloud (renaming files if necesary to avoid name conflicts).
  • iCloud => local. If the user is using iCloud and wants to turn iCloud off and use local files, we should ask the user what he wants to do. The options are “copy the iCloud files to local”, “keep files on iCloud”, or “switch back to using iCloud.”

Let’s start with the first case. We already have the stub for localToiCloud called in the proper situation (we covered this in part 3). So make the following changes to PTKMasterViewController.m:

// Add a new private instance variable
BOOL _moveLocalToiCloud;
 
// Replace localToiCloud with the following (and add a new method)
- (void)localToiCloudImpl {
    // TODO
}
 
- (void)localToiCloud {
    NSLog(@"local => iCloud");
 
    // If we have a valid list of iCloud files, proceed
    if (_iCloudURLsReady) {
        [self localToiCloudImpl];
    } 
    // Have to wait for list of iCloud files to refresh
    else {
        _moveLocalToiCloud = YES;         
    }
}
 
// Add to end of processiCloudFiles, right before enableUpdates
if (_moveLocalToiCloud) {            
    _moveLocalToiCloud = NO;
    [self localToiCloudImpl];            
}

OK, what’s going on here? Well, to move the files to iCloud we need to make sure there are no name conflicts. But we can’t know what files are in iCloud until the list of files returns from our NSMetadataQuery. So if the list of iCloud URLs isn’t ready, we set a flag to YES. When the metadata query results come in, we check this flag and call localToiCloudImpl (which will do the actual work) now that we have a valid list of filenames.

Next we need to make a slight tweak to getDocFilename so it can search in the list of iCloudURLs (instead of the list of PTKEntries). Make the following changes:

// Add right above getDocFilename:uniqueInObjects
- (BOOL)docNameExistsIniCloudURLs:(NSString *)docName {
    BOOL nameExists = NO;
    for (NSURL * fileURL in _iCloudURLs) {
        if ([[fileURL lastPathComponent] isEqualToString:docName]) {
            nameExists = YES;
            break;
        }
    }
    return nameExists;
}
 
// Replace TODO in docFilename:uniqueInObjects with this
nameExists = [self docNameExistsIniCloudURLs:newDocName];

So by passing in NO to docNameExists:uniqueInObjects we can have it make sure the name is unique in the iCloud URLs (even if iCloud if off).

Next replace localToiCloudImpl with the following:

- (void)localToiCloudImpl {
 
    NSLog(@"local => iCloud impl");
 
    NSArray * localDocuments = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:self.localRoot includingPropertiesForKeys:nil options:0 error:nil];
    for (int i=0; i < localDocuments.count; i++) {
 
        NSURL * fileURL = [localDocuments objectAtIndex:i];
        if ([[fileURL pathExtension] isEqualToString:PTK_EXTENSION]) {
 
            NSString * fileName = [[fileURL lastPathComponent] stringByDeletingPathExtension];
            NSURL *destURL = [self getDocURL:[self getDocFilename:fileName uniqueInObjects:NO]];
 
            // Perform actual move in background thread
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
                NSError * error;
                BOOL success = [[NSFileManager defaultManager] setUbiquitous:self.iCloudOn itemAtURL:fileURL destinationURL:destURL error:&error];
                if (success) {
                    NSLog(@"Moved %@ to %@", fileURL, destURL);
                    [self loadDocAtURL:destURL];
                } else {
                    NSLog(@"Failed to move %@ to %@: %@", fileURL, destURL, error.localizedDescription); 
                }
            });
 
        }
    }
 
}

This is the meat of the move logic. We loop through all of our local files, and get a unique filename for the local filename in iCloud. We then move the document to iCloud, which you can do by calling the setUbiquitous:itemAtURL:destinationItem method.

Compile and run the app, and make sure iCloud is off. Create a local document, and turn iCloud back on. Your local document should move to iCloud!

A local document moved to iCloud in PhotoKeeper

Copying Files From iCloud

Now let’s try the other way around. Make the following changes to PTKMasterViewController.m:

// Add new instance variable
BOOL _copyiCloudToLocal;
 
// Replace iCloudToLocal and add a stub
- (void)iCloudToLocalImpl {
 
    NSLog(@"iCloud => local impl");
    // TODO
}
 
- (void)iCloudToLocal {
    NSLog(@"iCloud => local");
 
    // Wait to find out what user wants first
    UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"You're Not Using iCloud" message:@"What would you like to do with the documents currently on this iPad?" delegate:self cancelButtonTitle:@"Continue Using iCloud" otherButtonTitles:@"Keep a Local Copy", @"Keep on iCloud Only", nil];
    alertView.tag = 2;
    [alertView show];
 
}
 
// Add second case in alertView:didDismissWithButtonIndex
// @"What would you like to do with the documents currently on this iPad?" 
// Cancel: @"Continue Using iCloud" 
// Other 1: @"Keep a Local Copy"
// Other 2: @"Keep on iCloud Only"
else if (alertView.tag == 2) {
 
    if (buttonIndex == alertView.cancelButtonIndex) {
 
        [self setiCloudOn:YES];
        [self refresh];
 
    } else if (buttonIndex == alertView.firstOtherButtonIndex) {
 
        if (_iCloudURLsReady) {
            [self iCloudToLocalImpl];
        } else {
            _copyiCloudToLocal = YES;
        }
 
    } else if (buttonIndex == alertView.firstOtherButtonIndex + 1) {            
 
        // Do nothing
 
    } 
 
}
 
// Add to end of processiCloudFiles, right before call to enableUpdates
else if (_copyiCloudToLocal) {
    _copyiCloudToLocal = NO;
    [self iCloudToLocalImpl];
}

This follows a similar strategy to what we did before, except we give the user a chance to choose what to do first.

Next replace iCloudToLocalImpl with the following:

- (void)iCloudToLocalImpl {
 
    NSLog(@"iCloud => local impl");
 
    for (NSURL * fileURL in _iCloudURLs) {
 
        NSString * fileName = [[fileURL lastPathComponent] stringByDeletingPathExtension];
        NSURL *destURL = [self getDocURL:[self getDocFilename:fileName uniqueInObjects:YES]];
 
        // Perform copy on background thread
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {            
            NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
            [fileCoordinator coordinateReadingItemAtURL:fileURL options:NSFileCoordinatorReadingWithoutChanges error:nil byAccessor:^(NSURL *newURL) {
                NSFileManager * fileManager = [[NSFileManager alloc] init];
                NSError * error;
                BOOL success = [fileManager copyItemAtURL:fileURL toURL:destURL error:&error];                     
                if (success) {
                    NSLog(@"Copied %@ to %@ (%d)", fileURL, destURL, self.iCloudOn);
                    [self loadDocAtURL:destURL];
                } else {
                    NSLog(@"Failed to copy %@ to %@: %@", fileURL, destURL, error.localizedDescription); 
                }
            }];
        });
    }
 
}

Here we use NSFileManager’s copyItemAtURL to copy the file, making sure to wrap it in an NSFileCoordinator for safety.

Compile and run the app, and make sure iCloud is enabled. Then switch to Settings and disable iCloud, and this will appear:

Prompt when iCloud switched off

If you tap “Keep a Local Copy”, you will see the items all copied locally for you! :]

Where To Go From Here?

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

Congratulations, you have made a semi-real world document-based iCloud app! It can do all the basic CRUD operations, as well as the more tricky bits like handling conflicts, handling both local and iCloud documents, and having the capability to switch documents between the two.

I don’t claim to have done everything in the best possible manner in this tutorial or made no mistakes. As such, I have put this project on GitHub in case anyone wants to take it from here and add any fixes/improvements moving forward.

I hope this tutorial has been useful to anyone trying to get iCloud working with a document-based app. If you have any questions, comments, 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.

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

29 Comments

[ 1 , 2 ]
  • I am also having the same issue as perlguy.

    I am using the project that the tutorial provided. Please find below steps to reproduce iCloud data not being downloaded to iOS Device.

    1. Install and Run projects provided by this tutorial.
    2. Insert a few new records.
    3. Delete the app from iOS Device.
    4. Re-install app on iOS Device and the records saved in iCloud are never downloaded.

    Any help is greatly appreciated. If a user deleted the app, then they reinstall it, the data from iCloud should automatically be re-downloaded for them.

    Thank you for this great tutorial.
    marco
  • Please add a Core Data tutorial using iCloud. Both from having an existing App or from the beginning, And for those Apps that are built using traditional IB or story mode.

    Thanks
    Nekbeth
  • So on iOS 5.1 my understanding is that we can use UIDocument for both the iCloud and no iCloud scenario interchangeably. In that case I assume that your coding of the 2 scenarios is unnecessary. Is that true??
    KeithTheBiped
  • In continuing my previous post above, I wanted t post an update on trying to figure out why this sample tutorial is not working.

    I am using the project that the tutorial provided. Please find below steps to reproduce iCloud data not being downloaded to iOS Device.

    1. Install and Run projects provided by this tutorial.
    2. Insert a few new records.
    3. Delete the app from iOS Device.
    4. Re-install app on iOS Device and the records saved in iCloud are never downloaded.

    Now that you have re-installed, the iCloud data should show up but it never opens... The fetch request finds the files in the iCloud but when the OpenWithCompletionHandler is called to open the Document, it does not give an error, it never fails or succeed.

    Code: Select all

    - (void)loadDocAtURL:(NSURL *)fileURL {
        NSLog(@"fileURL: %@",fileURL);
        // Open doc so we can read metadata
        Countdown * doc = [[Countdown alloc] initWithFileURL:fileURL];       
        [doc openWithCompletionHandler:^(BOOL success) {
           
         NSLog("NEVER GETS INSIDE HERE ???"); // No matter what it should have at least failed...

            // Check status
            if (!success) {
           
            }
    }


    At this point,if you create a new record, it saves and the image and record show just fine in the table, but the previous records still in the iCloud are never listed or opened...

    Could it be that once you delete the app, reinstall and you run it, choose iCloud, it sees the documents on the iCloud but it does not know how to open them anymore because of the extension, file type? I know I might be reaching but it does not make sense to me.

    I anyone can shed some light why its not working it is really, really appreciated.

    Customers expect the iCloud to be their backup, if they format they device and reinstall the app, the data should be able to be opened...

    Thank you so much.
    marco
  • Just wanted to check back and see if anyone else has had the issue I am having with my post above? Will there be a tutorial for iCloud for iOS 6?

    Thank you.
    marco
  • Is there some trick to avoid getting duplicates when you deactivate iCloud (local copy docs) then reactivate?

    Maybe check for duplicate documents before calling setUbiquitous?
    Javieralog
  • Ray thanks for the tutorial.

    I try to change the app, but I don't understand how can I insert an array of images from the photo gallery of the device in PTKDetailViewController,in window detail I created 6 UIImageView, when I press a UIImageView open the photo gallery of the device and I select an image, so I have 6 different image, but when selecting Done, I did not save all the images, but I only save one image for all 6, so when I select a cell and open the datail, I have 6 different image, I have a single image for all 6 UIImageView, someone can help me?

    this is my code:

    - (void)configureView
    Code: Select all
    - (void)configureView{
    // Update the user interface for the detail item.

    self.title = [self.doc description];

    if (self.doc.photo) {

    self.imageView.image = self.doc.photo;
    self.imageView2.image = self.doc.photo;
    self.imageView3.image = self.doc.photo;
    self.imageView4.image = self.doc.photo;
    self.imageView5.image = self.doc.photo;
    self.imageView6.image = self.doc.photo;

    } else {

    self.imageView.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView2.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView3.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView4.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView5.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView6.image = [UIImage imageNamed:@"defaultImage.png"];

    }

    }



    - (void)viewDidLoad
    Code: Select all
    - (void)viewDidLoad
    {
    [super viewDidLoad];

    UIBarButtonItem *backButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
    target:self
    action:@selector(doneTapped:)];

    self.navigationItem.leftBarButtonItem = backButtonItem;

    [self.navigationItem setHidesBackButton:YES animated:YES];

    UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView addGestureRecognizer:tapRecognizer];

    self.imageView.userInteractionEnabled = YES;


    UITapGestureRecognizer * tapRecognizer2 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView2 addGestureRecognizer:tapRecognizer2];
    self.imageView2.userInteractionEnabled = YES;

    UITapGestureRecognizer * tapRecognizer3 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView3 addGestureRecognizer:tapRecognizer3];
    self.imageView3.userInteractionEnabled = YES;

    UITapGestureRecognizer * tapRecognizer4 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView4 addGestureRecognizer:tapRecognizer4];
    self.imageView4.userInteractionEnabled = YES;

    UITapGestureRecognizer * tapRecognizer5 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView5 addGestureRecognizer:tapRecognizer5];
    self.imageView5.userInteractionEnabled = YES;

    UITapGestureRecognizer * tapRecognizer6 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView6 addGestureRecognizer:tapRecognizer6];
    self.imageView6.userInteractionEnabled = YES;

    [self configureView];

    }



    - (void)imageTapped:(UITapGestureRecognizer *)recognizer
    Code: Select all
    - (void)imageTapped:(UITapGestureRecognizer *)recognizer {

    UITapGestureRecognizer *tapGesture = (UITapGestureRecognizer *)recognizer;

    tappedImageView = (UIImageView *)[tapGesture view];

    if (!_picker) {

    _picker = [[UIImagePickerController alloc] init];
    _picker.delegate = self;
    _picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    _picker.allowsEditing = NO;

    }

    [self presentViewController:_picker animated:YES completion:nil];

    }


    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
    Code: Select all
    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {

    UIImage *image = (UIImage *) [info objectForKey:UIImagePickerControllerOriginalImage];

    CGSize mainSize = tappedImageView.bounds.size;
    UIImage *sImage = [image imageByBestFitForSize:mainSize]; //[image scaleToFitSize:mainSize];

    self.doc.photo = sImage;
    tappedImageView.image = sImage;


    [self dismissViewControllerAnimated:YES completion:nil];

    }
    Akire
  • Akire wrote:Ray thanks for the tutorial.

    I try to change the app, but I don't understand how can I insert an array of images from the photo gallery of the device in PTKDetailViewController,in window detail I created 6 UIImageView, when I press a UIImageView open the photo gallery of the device and I select an image, so I have 6 different image, but when selecting Done, I did not save all the images, but I only save one image for all 6, so when I select a cell and open the datail, I have 6 different image, I have a single image for all 6 UIImageView, someone can help me?

    this is my code:

    - (void)configureView
    Code: Select all
    - (void)configureView{
    // Update the user interface for the detail item.

    self.title = [self.doc description];

    if (self.doc.photo) {

    self.imageView.image = self.doc.photo;
    self.imageView2.image = self.doc.photo;
    self.imageView3.image = self.doc.photo;
    self.imageView4.image = self.doc.photo;
    self.imageView5.image = self.doc.photo;
    self.imageView6.image = self.doc.photo;

    } else {

    self.imageView.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView2.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView3.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView4.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView5.image = [UIImage imageNamed:@"defaultImage.png"];
    self.imageView6.image = [UIImage imageNamed:@"defaultImage.png"];

    }

    }



    - (void)viewDidLoad
    Code: Select all
    - (void)viewDidLoad
    {
    [super viewDidLoad];

    UIBarButtonItem *backButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
    target:self
    action:@selector(doneTapped:)];

    self.navigationItem.leftBarButtonItem = backButtonItem;

    [self.navigationItem setHidesBackButton:YES animated:YES];

    UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView addGestureRecognizer:tapRecognizer];

    self.imageView.userInteractionEnabled = YES;


    UITapGestureRecognizer * tapRecognizer2 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView2 addGestureRecognizer:tapRecognizer2];
    self.imageView2.userInteractionEnabled = YES;

    UITapGestureRecognizer * tapRecognizer3 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView3 addGestureRecognizer:tapRecognizer3];
    self.imageView3.userInteractionEnabled = YES;

    UITapGestureRecognizer * tapRecognizer4 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView4 addGestureRecognizer:tapRecognizer4];
    self.imageView4.userInteractionEnabled = YES;

    UITapGestureRecognizer * tapRecognizer5 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView5 addGestureRecognizer:tapRecognizer5];
    self.imageView5.userInteractionEnabled = YES;

    UITapGestureRecognizer * tapRecognizer6 = [[UITapGestureRecognizer alloc] initWithTarget:self
    action:@selector(imageTapped:)];

    [self.imageView6 addGestureRecognizer:tapRecognizer6];
    self.imageView6.userInteractionEnabled = YES;

    [self configureView];

    }



    - (void)imageTapped:(UITapGestureRecognizer *)recognizer
    Code: Select all
    - (void)imageTapped:(UITapGestureRecognizer *)recognizer {

    UITapGestureRecognizer *tapGesture = (UITapGestureRecognizer *)recognizer;

    tappedImageView = (UIImageView *)[tapGesture view];

    if (!_picker) {

    _picker = [[UIImagePickerController alloc] init];
    _picker.delegate = self;
    _picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    _picker.allowsEditing = NO;

    }

    [self presentViewController:_picker animated:YES completion:nil];

    }


    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
    Code: Select all
    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {

    UIImage *image = (UIImage *) [info objectForKey:UIImagePickerControllerOriginalImage];

    CGSize mainSize = tappedImageView.bounds.size;
    UIImage *sImage = [image imageByBestFitForSize:mainSize]; //[image scaleToFitSize:mainSize];

    self.doc.photo = sImage;
    tappedImageView.image = sImage;


    [self dismissViewControllerAnimated:YES completion:nil];

    }
    Akire
  • Ray & Team:
    Your tutorials are very helpful , Thanks for the wonderful tutorials .I have a small trouble with ordering the data when querying from documents/Ubiquity directory . Is it possible to sort the document's URL based on some field (say a String )inside the document . What i am doing currently is get the Document and then sort ,load it into tableview, which obviously looks weird. Is there any solution so that i get the sorted URL's of the document using query for keys in my document (Rather than the Predefined keys).
    shahid_s
  • Thanks for the tutorial!

    I have followed this tutorial to integrate iCloud into one of my apps. All appears to have gone well up to a point. On a single device , I am able to switch between iCloud on and iCloud off and everything updates as it should.

    However , if I load up the app on another device, the files on iCloud are not detected. After closer inspection , the files don't actually get synced to iCloud. If I try to delete the app on the first device, I get a warning informing my that the iCloud updates are not updated an I will lose that data if I continue.

    I realise that the tutorial was written for iOS5 but I can't see what , if anything , has changed for iOS7.

    Any help will be greatly appreciated have I have been left scratching my head for a few days now.
    Orochi_X
  • Thanks Ray for your helpful tutorial and your overall contribution.
    One question... what about the case that the user while in Detail View, presses the Home Button and then closes the app entirely?
    In my Xcode, the app crashes in that scenario... is there a solution for that? I guess the doc should be properly closed in the applicationWillResignActive of the AppDelegate, but couldn't get it to work...
    fmoust
  • Thanks for your efforts, Ray, to help people dive in iOS...

    I tried to make a very simple text editor to test the ideas you developed here about UIDocument <-> iCloud environment. Locally the application works nice. If I switch on iCloud, the App tries to move documents content (MyData encodes NSAttributedString) to iCloud, but I receive for all local documents a message in the debugger console:

    Failed to move file:///private/var/mobile/Applications/3BF26C57-6AE0-844E-8F1C-80D8A36C119C/Documents/Document2.thetext/ to file:///private/var/mobile/Library/Mobile%20Documents/XXXXXXXXX~fr~Tripledee~DocumentTest/Documents/Document2.thetext/: The operation couldnt be completed. (Cocoa error 516.)

    This code is a "file exist" code, from Apple Documentation. I checked iCloud, deleted any documents associated to my application on it... But this error is still there.

    When I erase everything, set iCloud on in settings, create a document, then switch off iCloud, and chose "Continue with iCloud" in the AlertView, I got a similar message in the debugger, saying that moving file from iCloud to local couldn't be completed (Cocoa error 516.)

    Any idea of what I missed? I checked twice all Entry, Data, Document, MasterViewController, DetailViewController classes code and find no difference (only the code I changed to manage a UITextView and its content). All iCloud and file management -motivated code is strictly the same. The PhotoKeeper works perfectly, of course ;-)
    Thanks for your help!
    Tripledee
  • Tripledee wrote:Thanks for your efforts, Ray, to help people dive in iOS...

    I tried to make a very simple text editor to test the ideas you developed here about UIDocument <-> iCloud environment. Locally the application works nice. If I switch on iCloud, the App tries to move documents content (MyData encodes NSAttributedString) to iCloud, but I receive for all local documents a message in the debugger console:

    Failed to move file:///private/var/mobile/Applications/3BF26C57-6AE0-844E-8F1C-80D8A36C119C/Documents/Document2.thetext/ to file:///private/var/mobile/Library/Mobile%20Documents/XXXXXXXXX~fr~Tripledee~DocumentTest/Documents/Document2.thetext/: The operation couldnt be completed. (Cocoa error 516.)

    This code is a "file exist" code, from Apple Documentation. I checked iCloud, deleted any documents associated to my application on it... But this error is still there.

    When I erase everything, set iCloud on in settings, create a document, then switch off iCloud, and chose "Continue with iCloud" in the AlertView, I got a similar message in the debugger, saying that moving file from iCloud to local couldn't be completed (Cocoa error 516.)

    Any idea of what I missed? I checked twice all Entry, Data, Document, MasterViewController, DetailViewController classes code and find no difference (only the code I changed to manage a UITextView and its content). All iCloud and file management -motivated code is strictly the same. The PhotoKeeper works perfectly, of course ;-)
    Thanks for your help!



    Hi!,

    I'm getting the same error when I try to move from local to iCloud and from iCloud to local. I verified everything several times, but I don't see what am I missing.
    andrea25
  • So far I've gotten a bit further on iOS 7, trying to adapt to this guide.

    I've noticed that the problem saving the files on iOS 7 is due to iCloud not creating a Document.extension/ folder on iCloud, but instead it's just uploading the .data and .metadata files. This, of course, doesn't work well for the app/guide.

    I haven't yet figured out a way to create a document directory/"container" and I'm starting to think that Apple doesn't even allow this (anymore?) and wants us to store them in either one single file, or in the Data directory (as opposed to Documents).

    EDIT: Well never mind that, had an extra else statement in getDocFilename which was causing a null filename to be generated. Now I'm also experiencing the "Failed to move file" problem and I'm still unable to actually save the files to iCloud, probably due to the document being a folder.

    EDIT 2: It seems like a common problem: http://stackoverflow.com/questions/1026 ... n-settings

    EDIT 3: After much trial and error, I tweaked the UTI settings and fiddled with the iCloud Containers, and saving now seems to work as intended, phew!
    Didstopia
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in July: Facebook Pop Tech Talk!

Sign Up - July

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Tony Dahbura

... 50 total!

Update Team

  • Andy Pereira

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Jose De La Roca
  • Miguel Angel
  • Victor Grushevskiy

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!