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

Ray Wenderlich

This post is also available in: Korean

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

15 Comments

  • Another Great Tut! Thanks, looking forward to the series!
    Elliott
  • When you say "If your app has a small amount of data to store (< 1 MB), this method is usually the best choice"

    Do you mean 1mb per user of the app, or for the data storage of all the users combined?
    GameViewPoint
  • 1MB per iCloud account ("user").
    rwenderlich
  • :D NICE :D
    elpuerco63
  • This is great Ray. Thanks a ton for putting this together.
    daviddelmonte
  • Thanks Ray, this is great.

    A couple of things were confusing to me as a beginner. I have worked primarily with Big Nerd Ranch and they don't follow the ivar = _ivar convention you are using.

    For instance in

    Code: Select all
    @synthesize photo = _photo;

    - (id)initWithPhoto:(UIImage *)photo {
        if ((self = [super init])) {
            self.photo = photo;
        }
        return self;
    }


    Would it be the same if you wrote self.photo = photo; or if you wrote _photo = photo; ? I mean self.photo or _photo is the instance variable while photo is only that which has been passed to the init method, right? It's a bit confusing.

    Also, I found it a bit convoluted to put all the meta data loading, file creation etc. in the MasterViewController. Don't know how to do it differently, though.

    Finally, I was wondering if it would make sense to work with a "store" for your images, i.e. a singleton which is accessible pretty much everywhere in the app. Again, pretty much like Big Nerd Ranch and their Homepwner Application if you are familiar with the book.

    Otherwise, thanks so much for this first part. I will continue to work my way through the rest. Thanks so much for tackling such a rather complex and substantial problem. WELL DONE!
    n.evermind
  • Hi Ray. I've gone over this a couple times, and am a little unclear on the following statement. Can you please expand?
    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.
    DenVog
  • Ray, thank you for the massive effort that had to go into this, both to just do it but also to boil things down to pretty much the essentials.

    I have a couple of questions or corrections to suggest, but only because I have been studying this, pouring over it, copying it, modifying it, etc. into my own project and I'm left with a couple of things that I'm scratching my head over. They're probably just vestigial things, but I'll bring them up for your own comment and maybe others unfortunate enough to be like me won't have to scratch their heads later :roll:

    1) In your code listing for PTKData.m you encode the integer 1 with kVersionKey, but you decode it into nothingness. I assume that means you don't really care in this app, but I worry that I'm maybe missing something subtle about decoders where maybe everything must be decoded regardless of whether or not you're going to use it. (the same practice is used in PTKMetadata.m as well, but that's just as likely to be cut/paste or duplicated code as much as it is intentional coding)

    2) In your code listing for PTKDocument.h you forward declare the class PTKData but don't do anything with that class in the interface. In the implementation you do, but there you've included the PTKData.h file. Is there something I'm missing about the forward declaration that makes it useful in this case?

    3) You code with a relationship between initWithCoder: and initWithPhoto: (as well as initWithThumbnail:) that leaves me wondering about your barebones init method. I code with the same "if ((self = [super init])) {}" out of paranoia; but in your init method you call a method on self that -- correct me if I'm wrong -- won't exist if (self = [super init]) failed. I have to believe the chances of it failing are remote in the worst way, but still, why be paranoid in one place and not the other?

    4) In the trimmed down example that is used in this tutorial series, the one approach here that I struggle with in adapting to a larger project is having initWithCoder: call initWithOneParameter :-) If the example were a little more full-bodied, take iTunes as an example, you might need to decode album art, rating, play count, etc. So suddenly that simple initWithOneParameter: method becomes a multiline nasty. I've been hurting my head thinking about how to *safely* call barebones init within initWithCoder: and then decode right into my instance variables, finally returning my "filled out" instance of self to the caller. By "safely" I'm referring to item 3 above :-)

    Well, that's enough for now. I hope this remains an active topic that you're following -- otherwise you won't see my opening kudos!
    jtoly
  • @n.evermind

    Would it be the same if you wrote self.photo = photo; or if you wrote _photo = photo; ? I mean self.photo or _photo is the instance variable while photo is only that which has been passed to the init method, right? It's a bit confusing.


    I'll take a stab here. I've been struggling with this myself a lot. I completely understand what an instance variable is. The definition of a property is a little more elusive. I totally get that a property represents more than just a store location: it includes a getter and a setter. I also understand completely that if you synthesize a property the compiler builds the getter and setter methods for you following standard conventions. But where the property was storing whatever it was setting and then getting was not fully formed for me until I worked through http://www.iphonedevsdk.com/forum/iphone-sdk-tutorials/26587-slicks-definitive-guide-properties.html and pushed, prodded and played with variations on how you define these things until I started to see compiler warnings that I could predict! All the practice Ray and others (some of whom are Apple) is providing is a notion that by setting the storage that backs up the property to a name different than the property you can more easily find mistakes where you are accessing the storage directly instead of through the getter/setter; and while all I ever read just said "so you know when you've made a mistake" I couldn't find any description of what the mistake would mean. But I've come to believe that if you do lazy loading in your getter, rather than using a synthesized loader, then accessing the storage instance variable directly would be *bad* because lazy loading wouldn't take place.

    Or you might do something "extra" in your setter, depending on the needs of your app. For example, if you have MyClass.h wherein you define a property myData, and then in MyClass.m you use @synthesize myData = _myData; -- and that's it (no providing your own getter/setter) then you only need to know at any given time whether you want to go through the getter (self.myData or [self myData]) or directly to the instance variable (_myData) and it probably won't matter which you choose (chime in folks and enlighten us all!!) because there's nothing happening via a getter or setter that would change what's going on. However, suppose you really wish iOS had bindings like OS X and so you provide your own setter so that whenever the value of your instance variable is to be changed you first post an NSNotification about it changing... if you then change it via the instance variable name (on accident you use _myData = newValue;) then your notification never gets sent and BadThings begin rolling downhill.

    Also, I found it a bit convoluted to put all the meta data loading, file creation etc. in the MasterViewController. Don't know how to do it differently, though.


    The flow of your app is going to dictate where you do all this. If your MasterViewController is actually doing something with the data then yeah, you either do it there or maybe in your AppDelegate (keep in mind though that viewDidLoad on your first view controller will get called before the app delegate receives applicationDidFinishLaunchingWithOptions:). If you look at the Keynote.app from Apple, there is what I would call a Welcome view controller that doesn't need to do anything with documents, and it is followed (the first time) by a Getting Started view controller with help/orientation content. It isn't until the third view controller that documents start to show up, so I suspect they're either handling this in the app delegate or in that later view controller. Choosing to do it in a view controller that is all about picking a document to work on encapsulates all that work nicely, but then means you have to use the prepareForSegue: (if using storyboards) to pass along information about the file or its data to the view controllers down the line.

    Finally, I was wondering if it would make sense to work with a "store" for your images, i.e. a singleton which is accessible pretty much everywhere in the app. Again, pretty much like Big Nerd Ranch and their Homepwner Application if you are familiar with the book.


    Well in essence the subclass of UIDocument is serving the same purpose as the store does in Homepwner. (I worked through that book too, and have nearly destroyed it with sticky notes, paper clips, dogeared pages, etc.) If your app is basically document-based, I think Ray's approach here will make the most sense to you when you inevitably return to this codebase several months (projects?) from now. But that comes back to the earlier notion of passing along to subsequent view controllers the references they will need from your document to do their work. You either keep it in a common store, make that available from the app delegate via a SharedAppDelegate macro, or you pass it along via calls into destination view controllers.
    jtoly
  • My question is, what if your app stores documents in a pre-determined format, like XML? Can you use UIDocument for this?

    Everything I've read seems to indicate that to use iCloud at all easily, you've got to give in to Apple's proprietary file data format. All those NSOBjects and such are stored in some manner that isn't easily spelled out. What if you just want to deal with files constructed with good old fwrite's and freads, where you know what each byte in the file means? Can you still use iCloud?

    I have been wanting to use iCloud for a while, but I can't find an answer to this question. Do you know?

    Thanks
    Bob
    bsabiston
  • no answer? I think a lot of people are hesitant to use iCloud because the documentation on this issue is so unclear. Does anyone know? I've had no luck on Apple's Developer forums.

    Thanks
    Bob
    bsabiston
  • Ray, Thanks for great tutorial.

    I'm really wondering if I can do same thing with sqlite file.
    Specificly it's a sqlite file of core data.

    and I also upload question on the stackoverflow.
    http://stackoverflow.com/questions/1472 ... e-data-int

    Thanks. waiting your reply.

    Thanks

    Bright Lee.
    benevbright
  • Hi Ray,

    Thanks for the tutorial! It is very helpful! I just have a question. I'm trying to encode a document that instead of a picture, has text and it has to be assigned to a TextView. What can I put instead of "NSData * photoData = UIImagePNGRepresentation(self.photo)";? I have try many things, but none of them have worked. When I open the document, the textView is blank, is not saving the text.
    andrea25
  • how to email attach PTK file????
    Harshit

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 May: Procedural Level Generation in Games with Kim Pedersen.

Sign Up - May

Coming up in June: WWDC Keynote - Podcasters React! with the podcasting team.

Sign Up - June

Vote For Our Next Book!

Help us choose the topic for our next book we write! (Choose up to three topics.)

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Adam Burkepile
  • Barbara Reichart

... 55 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Lin Ma
  • Sonic Zhao
  • Heejun Han

... 38 total!

Subject Matter Experts

  • Richard Casey

... 4 total!