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

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.

Contributors

Over 300 content creators. Join our team.