iCloud and UIDocument: Beyond the Basics, Part 1/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.

Creating a document-based iCloud app is complicated. There’s a lot of different things to think about, and it’s easy to forget to implement something out or make a mistake along the way.

There are several tutorials on creating iCloud apps out there, including Apple’s Your Third iOS App tutorial and our own iOS 5 by Tutorials, which do a great job of explaining the basics of using iCloud.

What we really need is a tutorial that goes beyond the basics and puts everything together into a complete app that you (gasp) might actually be able to use. This is my attempt to put something like that together for you guys!

In this tutorial, we’re going to create a simple iCloud document based app called PhotoKeeper. It will show you how to do the following:

  • Create, Read, Update, and Delete documents – both on iCloud *and* locally
  • Create documents consisting of multiple files with NSFileWrapper
  • Give previews of documents in the master view in an efficient manner
  • Allow a toggle for the user to enable/disable iCloud in Settings
  • Move/copy files to and from iCloud when user switches
  • Allow renaming and deleting files – the safe way
  • Deal with conflicts and document updates

This is a four-part tutorial series. In the first two parts, we’ll get things working for local documents only, and then in the second two parts, we’ll add iCloud support.

Are you ready to see how to put everything together with iCloud? Let’s get started!

Choosing our iCloud Storage Method

In this tutorial, we are going to create a simple app called PhotoKeeper that allows users to store and name their favorite photos either locally or in iCloud.

There are three main ways an app can store its data in iCloud. Which should we choose for this app?

  1. Use Key/Value Store. iCloud has an extremely easy to use helper class called NSUbiquitousKeyValueStore that is very similar to NSUserDefaults. If your app has a small amount of data to store (< 1 MB), this method is usually the best choice.
  2. Use Core Data. You can set up Core Data to store its data on iCloud. If your app doesn’t have the concept of documents or files but has a large amount of data, this method is usually the best choice.
  3. Create a Document-Based App. The final method is to create a document based app by subclassing a class called UIDocument. If your app is based around the concept of individual documents that the user can create/read/update/delete that you want listed as separate files in Settings, this method is usually the best choice.

For PhotoKeeper, the third option makes the most sense. We want each photo treated as a separate unit/document/file that we can see in iCloud storage separately.

If you think Key/Value Storage or iCloud makes more sense for your app, check out iOS 5 by Tutorials, where we have some examples and more details on those techniques.

iCloud Document-Based App Overview

Next let’s give an overview of what it takes to make an iCloud Document-Based App, and some design choices we’re going to make for PhotoKeeper along the way. We will cover five topics:

  1. How it Works
  2. Subclassing UIDocument
  3. Input/Output Formats
  4. Local vs. iCloud
  5. Storage Philosophy

1) How it Works

At a very high level, here’s how iCloud and UIDocument works.

If iCloud is available and your app is configured to use iCloud, your app can access a special directory on the device. Whatever you put in this directory will be automatically synchronized to iCloud by a system daemon.

You can’t just enumerate this directory with normal APIs – you have to use a special API we’ll discuss later. You also have to be careful about accessing files in this directory, because the the iCloud daemon is also working with the files at the same time.

The easiest way to work with these files is by subclassing a class called UIDocument. It takes care most of the details for you (such as coordinating with the iCloud daemon and more), and allows you to focus primarily on what matters to you – your app’s data!

For more details on how iCloud works, check out the the iOS App Programming Guide: iCloud Storage.

Via Apple's iOS Programming Guide.

Via Apple's iOS Programming Guide.

2) Subclassing UIDocument

When you subclass UIDocument you need to overrride two methods:

  • loadFromContents:ofType:error: Think of this as “read the document”. You get an input class, and you have to decode it to your internal model.
  • contentsForType:error: Think of this as “write the document”. You encode your internal model to an output class.

It’s usually best if your UIDocument class contains a reference to your app’s model class, which can be just a plain old NSObject. If your model object supports NSCoding (tutorial here), implementing these methods is trivial.

For this tutorial, we’ve kept our document’s data model very simple – it’s literally jsut a photo! However, you can use this same technique we’re demonstrating in this tutorial no matter how complex or large your docuemnt is.

3) Input/Output Formats

UIDocument supports two different classes for input/output:

  • NSData. This represents a simple buffer of data, which is good when your document is just a single file.
  • NSFileWrapper. This represents a directory of file packages, which the OS treats as a single file. This is good for when your document consists of multiple files that you want to be able to load independently.

At first glance, it might seem that using NSData would make the most sense, since our document is just a simple photo.

However, one of the design goals of PhotoKeeper is to show a thumbnail of the photo in the master view controller before the user opens a file. If we used NSData, to get the thumbnails we’d have to open and decode every single document from the disk. Since the images can be quite large, this could lead to slow performance and high memory overhead.

So what we’re going to do instead is use NSFileWrapper. We’ll store two documents inside the wrapper:

  1. The main data. This will be the main data of the document. For PhotoKeeper, this will be our full size photo.
  2. The metadata. This will be anything the master view controller needs to display a preview. It should be a small amount of data that can be loaded quickly. For PhotoKeeper, this will be a small (resized) thumbnail of the photo.

This way the master view controller only needs to load each photo’s (small) metadata sub-file inside the NSWrapper instead of the entire (large) data sub-file. It might not make a huge difference in this tutorial, but the larger your document’s data file becomes the more useful this is!

4) Local vs. iCloud

It’s not enough to make an app that is just iCloud enabled – it also has to work without iCloud too!

This is because the user might not have iCloud enabled on their device, or may want to turn iCloud off specifically for your app.

One of the nice things about using UIDocument is you can use it for both local storage and iCloud storage, so you don’t have to have two widely different code paths.

However, adding iCloud support with UIDocument does have a lot of extra steps and subtle problems to address. So I personally find it easier to get your app working locally with UIDocument, and then adding iCloud support once that’s working. So that’s what we’re going to do in this series!

5) Storage Philosophy

It is theoretically possible to design an app that has two different tabs – iCloud and local – and allow users to select where to store individual documents.

However, it appears this is not a philosphy Apple wants us to follow. According to the Document-Based App Programming Guide for iOS:

All documents of an application are stored either in the local sandbox or in an iCloud container directory. A user should not be able to select individual documents for storage in iCloud.

The same document also recommends that users have an option to disable iCloud for your app. I think the best place for this is in Settings, since it’s a setting that only needs to be changed rarely, reduces the clutter in your app, and discourages users from messing with it once they have it set up.

Putting it All Together

Phew – that’s a lot of background information! Don’t worry if you don’t understand everything right away, it will become more clear as we work through the tutorial.

To sum up what we discussed above, we will be doing the following for PhotoKeeper:

  • Choosing a Document-Based App as our storage method.
  • Creating a model class (a plain old NSObject that stores our large photo) that implements NSCoding.
  • Creating a metadata class (a plain old NSObject that stores our small thumbnail) that implements NSCoding.
  • Subclassing UIDocument. It will have a reference to the model and metadata classes.
  • Using NSFileWrapper for input/output. Our UIDocumentClass will encode both the model and metadata into separate files inside our NSFileWrapper directory.
  • Work both locally and for iCloud. We will start with local only and add iCloud support later in the series.
  • Add iCloud on/off toggle in Settings. All documents will be stored either locally or in iCloud.

Note that for your apps you might make different design decisions than the above – however if you’re UIDocument based most of this tutorial will still apply, so read on :]

Getting Started

It’s finally time to code!

Create a new project in Xcode and choose the iOS\Application\Master-Detail Application tepmlate. Enter PhotoKeeper for product name, PTK as the Class Prefix, select iPhone for the Device Family, and make sure Use Storyboards and Use Automatic Reference Counting are checked (but nothing else).

Creating a new project in Xcode

Next, download the resources for this project and add them to your Xcode project. These are just two simple classes to help format a string or resize images and a few images – take a peek if you want.

Compile and run the project, and you’ll see the template has generated a skeleton of a simple master/detail app. You can tap the + button to add entries, and tap an entry to go to a detail screen.

The Master/Detail template in Xcode

Our goal in this first part of the series is to switch the master view to showing a list of local UIDocuments, and allow the + button to create a new UIDocument.

Before we can create a UIDocuemnt subclass though, we need to create our model classes.

Let’s start with the main model class. Create a new file with the iOS\Cocoa Touch\Objective-C class template, name it PTKData, and make it a subclass of NSObject. Then replace the contents of PTKData.h with the following:

#import <Foundation/Foundation.h>
 
@interface PTKData : NSObject <NSCoding>
 
@property (strong) UIImage * photo;
 
@end

And replace PTKData.m with the following:

#import "PTKData.h"
 
@implementation PTKData
@synthesize photo = _photo;
 
- (id)initWithPhoto:(UIImage *)photo {
    if ((self = [super init])) {
        self.photo = photo;
    }
    return self;
}
 
- (id)init {
    return [self initWithPhoto:nil];
}
 
#pragma mark NSCoding
 
#define kVersionKey @"Version"
#define kPhotoKey @"Photo"
 
- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeInt:1 forKey:kVersionKey];
    NSData * photoData = UIImagePNGRepresentation(self.photo);
    [encoder encodeObject:photoData forKey:kPhotoKey];
}
 
- (id)initWithCoder:(NSCoder *)decoder {
    [decoder decodeIntForKey:kVersionKey];
    NSData * photoData = [decoder decodeObjectForKey:kPhotoKey];
    UIImage * photo = [UIImage imageWithData:photoData];
    return [self initWithPhoto:photo];
}
 
@end

As you can see, this is a very simple model class that only has one piece of data to track – the full size photo. It implements the NSCoding protocol to encode/decode to a buffer of data.

Note that it stores a version number along with the photo. This makes it easy to update the data structure if you want to in the future while still supporting older files. If you ever add a new field, you bump up the version number, then while decoding you can check the version number to see if the new field is available.

For more information on NSCoding, check out this tutorial.

Next, let’s create the metadata model class. Again, the purpose of this class is so we can store a much smaller thumbnail version of the photo that will load quickly in the master view controller.

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

#import <Foundation/Foundation.h>
 
@interface PTKMetadata : NSObject <NSCoding>
 
@property (strong) UIImage * thumbnail;
 
@end

And replace PTKMetadata.m with the following:

#import "PTKMetadata.h"
 
@implementation PTKMetadata
@synthesize thumbnail = _thumbnail;
 
- (id)initWithThumbnail:(UIImage *)thumbnail {
    if ((self = [super init])) {
        self.thumbnail = thumbnail;
    }
    return self;
}
 
- (id)init {
    return [self initWithThumbnail:nil];
}
 
#pragma mark NSCoding
 
#define kVersionKey @"Version"
#define kThumbnailKey @"Thumbnail"
 
- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeInt:1 forKey:kVersionKey];
    NSData * thumbnailData = UIImagePNGRepresentation(self.thumbnail);
    [encoder encodeObject:thumbnailData forKey:kThumbnailKey];
}
 
- (id)initWithCoder:(NSCoder *)decoder {
    [decoder decodeIntForKey:kVersionKey];
    NSData * thumbnailData = [decoder decodeObjectForKey:kThumbnailKey];
    UIImage * thumbnail = [UIImage imageWithData:thumbnailData];
    return [self initWithThumbnail:thumbnail];
}
 
@end

This is pretty much exactly the same as PTKData, so no need to discuss further here. We even could have used the same class, but it’s better to keep things separate in case we need more data fields in the future (which will be the case for most apps).

Congrats – now you have the model classes for PhotoKeeper!

Subclassing UIDocument

Next we are going to create a subclass of UIDocument that will combine the data and metadata into a single NSFileWrapper.

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

#import <UIKit/UIKit.h>
 
@class PTKData;
@class PTKMetadata;
 
#define PTK_EXTENSION @"ptk"
 
@interface PTKDocument : UIDocument
 
// Data
- (UIImage *)photo;
- (void)setPhoto:(UIImage *)photo;
 
// Metadata
@property (nonatomic, strong) PTKMetadata * metadata;
- (NSString *) description;
 
@end

Even though NSFileWrapper is basically a directory, we still need to give it a file extension so we can identify the directory as a document our app knows how to handle. We’ll use “ptk” as our file extension, so we define it here for easy access later.

Rather than letting users of PTKDocument access the PTKData directly, it’s best if you add accessors instead. This is because UIDocument is built around the concept of undo/redo – it has an undo manager and when you make changes UIDocument can auto-save in the background. Accessors are the perfect spot to register your undo actions, which you’ll see in a minute.

It’s OK if the user accesses the metadata directly though, as it’s not something the app will modify. Instead, the metadata will be automatically updated when the user sets the photo.

Next open PTKDocument.m. There’s a lot of code we’re going to add here, so let’s go over it bit by bit.

#import "PTKDocument.h"
#import "PTKData.h"
#import "PTKMetadata.h"
#import "UIImageExtras.h"
 
#define METADATA_FILENAME   @"photo.metadata"
#define DATA_FILENAME       @"photo.data"
 
@interface PTKDocument ()
@property (nonatomic, strong) PTKData * data;
@property (nonatomic, strong) NSFileWrapper * fileWrapper;
@end
 
@implementation PTKDocument 
@synthesize data = _data;
@synthesize fileWrapper = _fileWrapper;
@synthesize metadata = _metadata;

First we import the headers we need and define the filenames for the two sub-files that will be inside our directory/NSFileWrapper.

Then we create two private properties – one for the main document model class (PTKData), and one for the NSFileWrapper (the class that treats the directory as a single file). Finally we synthseize our properties.

Next add the code to write the UIDocument to disk:

- (void)encodeObject:(id<NSCoding>)object toWrappers:(NSMutableDictionary *)wrappers preferredFilename:(NSString *)preferredFilename {
    @autoreleasepool {            
        NSMutableData * data = [NSMutableData data];
        NSKeyedArchiver * archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
        [archiver encodeObject:object forKey:@"data"];
        [archiver finishEncoding];
        NSFileWrapper * wrapper = [[NSFileWrapper alloc] initRegularFileWithContents:data];
        [wrappers setObject:wrapper forKey:preferredFilename];
    }
}
 
- (id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
 
    if (self.metadata == nil || self.data == nil) {        
        return nil;    
    }
 
    NSMutableDictionary * wrappers = [NSMutableDictionary dictionary];
    [self encodeObject:self.metadata toWrappers:wrappers preferredFilename:METADATA_FILENAME];
    [self encodeObject:self.data toWrappers:wrappers preferredFilename:DATA_FILENAME];   
    NSFileWrapper * fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:wrappers];
 
    return fileWrapper;
 
}

Take a look at contentsForType first. To create a directory NSFileWrapper, you call initDirectoryWithFileWrapper and pass in a dictionary that contains file NSFileWrappers as the objects, and the names of the files as the key. We use a helper method encodeObject:toWrappers:preferredFilename to create a file NSFileWrapper for the data and metadata.

The encodeObject:toWrappers:preferredFilename method will look familiar if you are have used NSCoding in the past. It uses the NSKeyedArchiver class to convert the object that implements NSCoding into a data buffer. Then it creates a file NSFileWrapper with the buffer and adds it to the dictionary.

Sweet! Now let’s implement reading. Add the following to the file next:

- (id)decodeObjectFromWrapperWithPreferredFilename:(NSString *)preferredFilename {
 
    NSFileWrapper * fileWrapper = [self.fileWrapper.fileWrappers objectForKey:preferredFilename];
    if (!fileWrapper) {
        NSLog(@"Unexpected error: Couldn't find %@ in file wrapper!", preferredFilename);
        return nil;
    }
 
    NSData * data = [fileWrapper regularFileContents];    
    NSKeyedUnarchiver * unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
 
    return [unarchiver decodeObjectForKey:@"data"];
 
}
 
- (PTKMetadata *)metadata {    
    if (_metadata == nil) {
        if (self.fileWrapper != nil) {
            //NSLog(@"Loading metadata for %@...", self.fileURL);
            self.metadata = [self decodeObjectFromWrapperWithPreferredFilename:METADATA_FILENAME];
        } else {
            self.metadata = [[PTKMetadata alloc] init];
        }
    }    
    return _metadata;
}
 
- (PTKData *)data {    
    if (_data == nil) {
        if (self.fileWrapper != nil) {
            //NSLog(@"Loading photo for %@...", self.fileURL);
            self.data = [self decodeObjectFromWrapperWithPreferredFilename:DATA_FILENAME];
        } else {
            self.data = [[PTKData alloc] init];
        }
    }    
    return _data;
}
 
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
 
    self.fileWrapper = (NSFileWrapper *) contents;    
 
    // The rest will be lazy loaded...
    self.data = nil;
    self.metadata = nil;
 
    return YES;
 
}

Start at the bottom with loadFromContents:ofType:error:. Notice how all we do is squirrel the passed in contents (a directory NSFileWrapper, since that’s what we saved) into a property. We don’t actually load the data right away (although you can do that for your app if you want), since for this app we want to lazy load instead.

Lazy loading is a fancy way of saying “don’t load it until I ask for it.” This is good for our app because the master view controller only needs to load the metadata. The full data is only needed when you go to the detail view controller.

If you look at the accessors for metadata and data, they check to see if there is a directory NSFileWrapper stored, and if there is they read the appropriate file from the directory NSFileWrapper.

Finally, decodeObjectFromWrapperWithPreferredFilename does the opposite of encodeObject:toWrappers:preferredFilename. It reads the appropriate file NSFileWrapper from the directory NSFileWrapper and converts the NSData contents back to an object via the NSCoding protocol.

Almost done! Just add this to the bottom of the file:

- (NSString *) description {
    return [[self.fileURL lastPathComponent] stringByDeletingPathExtension];
}
 
#pragma mark Accessors
 
- (UIImage *)photo {
    return self.data.photo;
}
 
- (void)setPhoto:(UIImage *)photo {
 
    if ([self.data.photo isEqual:photo]) return;
 
    UIImage * oldPhoto = self.data.photo;
    self.data.photo = photo;
    self.metadata.thumbnail = [self.data.photo imageByScalingAndCroppingForSize:CGSizeMake(145, 145)];
 
    [self.undoManager setActionName:@"Image Change"];
    [self.undoManager registerUndoWithTarget:self selector:@selector(setPhoto:) object:oldPhoto];
}
 
@end

Description returns the filename without the path or extension, and photo is a simple getter.

The important part is setPhoto. Not only does this set the photo in the data, but this also creates a smaller thumbnail image and saves it in the metadata.

It also registers an undo action so that the user can undo the change (via shaking on the iPhone, or you can add a custom button). By doing this, internally UIDocument now knows that the document has been modified, so it will auto-save periodically in the background.

w00t – now you have both the data model and UIDocument working! Now it’s time to retrofit our master view controller to use it.

Creating Documents

Before we can display a list of documents, we need to have the ability to add at least one so we have something to look at. So let’s start by imiplementing creating a new document.

There are four things you need to do in order to create a new document in this app:

  1. Store Entries
  2. Find an Available URL
  3. Create the Document
  4. Make Final Changes

Let’s go over these one by one!

Storing Entries

If you look at PTKMasterViewController.m, you’ll see that viewDidLoad adds a button to the toolbar that the user can tap to create a new entry:

UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)];
self.navigationItem.rightBarButtonItem = addButton;

When the button is tapped, insertNewObject is called. The current implementation puts an NSDate in an array, which is displayed in a table view.

Instead of displaying NSDates, we want to display information about our documents. For each document, we’ll need to know its file URL, its metadata (thumbnail), the date it was last modified, and what state the document is in (more on this later).

To keep track of all of this information we need, let’s create a new class real quick. Create a new file with the iOS\Cocoa Touch\Objective-C class template, name it PTKEntry, and make it a subclass of NSObject. Then replace the contents of PTKEntry.h with the following:

#import <Foundation/Foundation.h>
 
@class PTKMetadata;
 
@interface PTKEntry : NSObject
 
@property (strong) NSURL * fileURL;
@property (strong) PTKMetadata * metadata;
@property (assign) UIDocumentState state;
@property (strong) NSFileVersion * version;
 
- (id)initWithFileURL:(NSURL *)fileURL metadata:(PTKMetadata *)metadata state:(UIDocumentState)state version:(NSFileVersion *)version;
- (NSString *) description;
 
@end

This is a very simple class that just keeps track of all of the items we just discussed.

Next switch to PTKEntry.m and add the implementation, which is simple as well:

#import "PTKEntry.h"
 
@implementation PTKEntry
 
@synthesize fileURL = _fileURL;
@synthesize metadata = _metadata;
@synthesize state = _state;
@synthesize version = _version;
 
- (id)initWithFileURL:(NSURL *)fileURL metadata:(PTKMetadata *)metadata state:(UIDocumentState)state version:(NSFileVersion *)version {
 
    if ((self = [super init])) {
        self.fileURL = fileURL;
        self.metadata = metadata;
        self.state = state;
        self.version = version;
    }
    return self;
 
}
 
- (NSString *) description {
    return [[self.fileURL lastPathComponent] stringByDeletingPathExtension];
}
 
@end

Finding an Available URL

The next step is to figure out the full URL for where we want to create the document. This isn’t as easy as it sounds, because we need to auto-generate a filename that isn’t already used. And the first step of doing that is having a way to check if a file exists.

To help solve this, let’s assume that the _objects array contains all the PTKEntries on the disk. Then all we have to do is look through this array to see if the name is already taken.

Let’s see what this looks like in code. Make the following changes to PTKMasterViewController.m:

// Add imports to top of file
#import "PTKDocument.h"
#import "NSDate+FormattedStrings.h"
#import "PTKEntry.h"
#import "PTKMetadata.h"'
 
// Add new private variables to interface
NSURL * _localRoot;
PTKDocument * _selDocument;
 
// Add new methods right after @implementation
#pragma mark Helpers
 
- (BOOL)iCloudOn {    
    return NO;
}
 
- (NSURL *)localRoot {
    if (_localRoot != nil) {
        return _localRoot;
    }
 
    NSArray * paths = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
    _localRoot = [paths objectAtIndex:0];
    return _localRoot;    
}
 
- (NSURL *)getDocURL:(NSString *)filename {    
    if ([self iCloudOn]) {        
        // TODO
        return nil;
    } else {
        return [self.localRoot URLByAppendingPathComponent:filename];    
    }
}
 
- (BOOL)docNameExistsInObjects:(NSString *)docName {
    BOOL nameExists = NO;
    for (PTKEntry * entry in _objects) {
        if ([[entry.fileURL lastPathComponent] isEqualToString:docName]) {
            nameExists = YES;
            break;
        }
    }
    return nameExists;
}

Let’s start from the top. We start by adding some imports we need, and add instance variables to keep track of the local root (i.e. the Documents directory) and the selected document when a user taps a row. We also define a method to return whether iCloud is on or off. For now we set it to always off, but we’ll come back to this later.

Next we write a getter for localRoot so it can be lazy loaded. If it isn’t set yet, we use find the URL for the documents directory and squirrel it away in the instance variable.

getDocURL is a method where you pass in a filename and get the full path to the file. If iCloud is off, this simply appends the filename to the Documents directory path. We’ll add the iCloud implementation later.

Finally, docNameExistsInObjects loops through the list of PTKEntry instances in _objects and checks to see if the name matches any of them. If it does, there’s a conflict, otherwise it’s OK.

Next add this method to find a non-taken name for a file:

- (NSString*)getDocFilename:(NSString *)prefix uniqueInObjects:(BOOL)uniqueInObjects {
    NSInteger docCount = 0;
    NSString* newDocName = nil;
 
    // At this point, the document list should be up-to-date.
    BOOL done = NO;
    BOOL first = YES;
    while (!done) {
        if (first) {
            first = NO;
            newDocName = [NSString stringWithFormat:@"%@.%@",
                          prefix, PTK_EXTENSION];
        } else {
            newDocName = [NSString stringWithFormat:@"%@ %d.%@",
                          prefix, docCount, PTK_EXTENSION];
        }
 
        // Look for an existing document with the same name. If one is
        // found, increment the docCount value and try again.
        BOOL nameExists;
        if (uniqueInObjects) {
            nameExists = [self docNameExistsInObjects:newDocName]; 
        } else {
            // TODO
            return nil;
        }
        if (!nameExists) {            
            break;
        } else {
            docCount++;            
        }
 
    }
 
    return newDocName;
}

This starts with the document name passed in and checks if it is available. If not, it adds 1 to the end of the name and tries again. If that fails it keeps incrementing the number until it finds an available name.

Creating a Document

To create a PTKDocument, there are two steps:

  1. Alloc/init the PTKDocument with the URL to save the file to.
  2. Call saveToURL to initially create the file.

After you call saveToURL, the document is effectively “opened” and in a usable state.

After we create the document, we need to update the objects array to store the document, and display the detail view controller.

Let’s add the code! First we need to add a helper method to add (or update) a PTKEntry in the _objects array. Add the following code right before awakeFromNib:

#pragma mark Entry management methods
 
- (int)indexOfEntryWithFileURL:(NSURL *)fileURL {
    __block int retval = -1;
    [_objects enumerateObjectsUsingBlock:^(PTKEntry * entry, NSUInteger idx, BOOL *stop) {
        if ([entry.fileURL isEqual:fileURL]) {
            retval = idx;
            *stop = YES;
        }
    }];
    return retval;    
}
 
- (void)addOrUpdateEntryWithURL:(NSURL *)fileURL metadata:(PTKMetadata *)metadata state:(UIDocumentState)state version:(NSFileVersion *)version {
 
    int index = [self indexOfEntryWithFileURL:fileURL];
 
    // Not found, so add
    if (index == -1) {    
 
        PTKEntry * entry = [[PTKEntry alloc] initWithFileURL:fileURL metadata:metadata state:state version:version];
 
        [_objects addObject:entry];
        [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:(_objects.count - 1) inSection:0]] withRowAnimation:UITableViewRowAnimationRight];
 
    } 
 
    // Found, so edit
    else {
 
        PTKEntry * entry = [_objects objectAtIndex:index];
        entry.metadata = metadata;    
        entry.state = state;
        entry.version = version;
 
        [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:index inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
 
    }
 
}
 
#pragma mark View lifecycle

This is fairly simple so just take a look through, no need to discuss much here.

Next replace the implementation of insertNewObject with the following:

- (void)insertNewObject:(id)sender
{
    // Determine a unique filename to create
    NSURL * fileURL = [self getDocURL:[self getDocFilename:@"Photo" uniqueInObjects:YES]];
    NSLog(@"Want to create file at %@", fileURL);
 
    // Create new document and save to the filename
    PTKDocument * doc = [[PTKDocument alloc] initWithFileURL:fileURL];
    [doc saveToURL:fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
 
        if (!success) {
            NSLog(@"Failed to create file at %@", fileURL);
            return;
        } 
 
        NSLog(@"File created at %@", fileURL);        
        PTKMetadata * metadata = doc.metadata;
        NSURL * fileURL = doc.fileURL;
        UIDocumentState state = doc.documentState;
        NSFileVersion * version = [NSFileVersion currentVersionOfItemAtURL:fileURL];
 
        // Add on the main thread and perform the segue
        _selDocument = doc;
        dispatch_async(dispatch_get_main_queue(), ^{
            [self addOrUpdateEntryWithURL:fileURL metadata:metadata state:state version:version];
            [self performSegueWithIdentifier:@"showDetail" sender:self];
        });
 
    }]; 
}

We’re finally putting all of the helper methods we wrote to good use! Here we:

  • Use getDocURL to find the local Documents directory
  • Use getDocFilename to find an available filename in that directory
  • Alloc/init a PTKDocument
  • Call saveToURL to save it right away
  • Keep track of the (opened) and selected document
  • Add the entry to the table, and perform the showDetail segue

Note that there is no guarantee that the completionHandler passed to saveToURL executes on the main thread, hence we have to use dispatch_async to run parts of the code on the main queue which need it.

Final Changes

OK, almost ready to test this out! Just make these final changes to PTKMasterViewController.m:

// Add to end of viewDidLoad
_objects = [[NSMutableArray alloc] init];
 
// Replace tableView:cellForRowAtIndexPath with the following
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
 
    PTKEntry *entry = [_objects objectAtIndex:indexPath.row];
    cell.imageView.image = entry.metadata.thumbnail;
    cell.textLabel.text = entry.description;
    cell.detailTextLabel.text = [entry.version.modificationDate mediumString];    
 
    return cell;
}
 
// Replace prepareForSegue with the following
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showDetail"]) {
        // TODO
    } 
}

Here we just initialize the objects array and configure our table view to display information about the documents. We’ll fix up the segue and detail view controller later.

Compile and run your project, and you should now be able to tap the + button to create new documents:

Creating local UIDocuments in PhotoKeeper

If you look at the console output, you should see messages showing the full path the documents are being saved to, like this:

File created at file://localhost/Users/rwenderlich/Library/Application%20Support/
iPhone%20Simulator/5.1/Applications/
194CFF63-BE3F-4CAF-90CC-BFA843C2169B/Documents/Photo%202.ptk

However this app has a big problem. If you run the app again, nothing will show up in the list! That’s because we haven’t added the code to list documents yet, so let’s do that now.

Listing Local Documents

To list local documents, we’re going to get the URLs for all the documents in the local Documents directory, and open up each one. We’ll read the metadata out to get the thumbnail (but not the data, so things are efficient), then close it back up again and add it to the table view.

First let’s add the method that loads a document given a file URL. Add this right above the “View lifecycle” section with awakeFromNib:

#pragma mark File management methods
 
- (void)loadDocAtURL:(NSURL *)fileURL {
 
    // Open doc so we can read metadata
    PTKDocument * doc = [[PTKDocument alloc] initWithFileURL:fileURL];        
    [doc openWithCompletionHandler:^(BOOL success) {
 
        // Check status
        if (!success) {
            NSLog(@"Failed to open %@", fileURL);
            return;
        }
 
        // Preload metadata on background thread
        PTKMetadata * metadata = doc.metadata;
        NSURL * fileURL = doc.fileURL;
        UIDocumentState state = doc.documentState;
        NSFileVersion * version = [NSFileVersion currentVersionOfItemAtURL:fileURL];
        NSLog(@"Loaded File URL: %@", [doc.fileURL lastPathComponent]);
 
        // Close since we're done with it
        [doc closeWithCompletionHandler:^(BOOL success) {
 
            // Check status
            if (!success) {
                NSLog(@"Failed to close %@", fileURL);
                // Continue anyway...
            }
 
            // Add to the list of files on main thread
            dispatch_async(dispatch_get_main_queue(), ^{                
                [self addOrUpdateEntryWithURL:fileURL metadata:metadata state:state version:version];
            });
        }];             
    }];
 
}

Notice that we open the document, get what we need, and close it again rather than keeping it open permanently. This is for two reasons:

  1. Avoid the overhead of keeping an entire UIDocument in memory when we just need one part
  2. UIDocuments can only be opened and closed once (!) UIDocument is a “one shot” class – you can just open it once and close it once. If you want to open the same fileURL again, you have to create a new UIDocument instance.

Right after this add these methods to perform the refresh:

#pragma mark Refresh Methods
 
- (void)loadLocal {
 
    NSArray * localDocuments = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:self.localRoot includingPropertiesForKeys:nil options:0 error:nil];
    NSLog(@"Found %d local files.", localDocuments.count);    
    for (int i=0; i < localDocuments.count; i++) {
 
        NSURL * fileURL = [localDocuments objectAtIndex:i];
        if ([[fileURL pathExtension] isEqualToString:PTK_EXTENSION]) {
            NSLog(@"Found local file: %@", fileURL);
            [self loadDocAtURL:fileURL];
        }        
    }
 
    self.navigationItem.rightBarButtonItem.enabled = YES;
}
 
- (void)refresh {
 
    [_objects removeAllObjects];
    [self.tableView reloadData];
 
    self.navigationItem.rightBarButtonItem.enabled = NO;
    if (![self iCloudOn]) {
        [self loadLocal];       
    }        
}

This is pretty simple file management stuff – loadLocal calls loadDocAtURL for everything in the Documents directory, and refresh kicks this all off.

The reason we enable/disable the right bar button item is you shouldn’t be able to create a new file until we have retrieved the list of files. This is because we need the list of files to guarantee a unique name when creating a new file. This doesn’t much matter now since we get the list of files right away, but is good to have in place for when that isn’t the case with iCloud.

To try this out, add the following to the bottom of viewDidLoad:

[self refresh];

Compile and run, and this time your app should correctly pick up the list of documents since last time it was run!

Where To Go From Here?

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

At this point, you have the data model and UIDocument wrapper working and the ability to create and list files. It might not look like much yet, but that’s the most critical part!

In the next part of the series, we’ll get this app fully functional and polished when it comes to local files. We’ll customize our table view cells, add a detail view, support deleting and renaming, and more! At that point, you’ll be fully set to move to the iCloud. :]

If you have any questions, comments, or suggestions about better ways to do things, 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

16 Comments

[ 1 , 2 ]
  • rwenderlich wrote:This is the official thread to discuss the following blog post: iCloud and UIDocument: Beyond the Basics, Part 1/4


    Hi Sir,

    Please guide me how can i download iphone backup from icloud using c++ on windows pc .
    beni123
[ 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!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Non-Gaming!

    Loading ... Loading ...

Last week's winner: How to Make a Simple 2D Game with Metal.

Suggest a Tutorial - Past Results

Hang Out With Us!

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


Coming up in October: Xcode 6 Tips and Tricks!

Sign Up - October

Our Books

Our Team

Tutorial Team

  • Tim Mitra
  • Matt Galloway

... 53 total!

Update Team

... 14 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

  • Richard Casey

... 4 total!