Apple’s UIDocument class is almost mandatory if you are going to be interacting with iCloud. Although, in theory, you can get by without using, there are a lot of file locking and other related concerns that it handles for you. Even if you’re not using iCloud, it makes for a nice way to save and load data from your application files. When writing your extension to UIDocument, there are a number of things you should beware of in order to have a robust application. This is what we will discuss in this tutorial.

First, however, we need to get ourselves set up with a basic UIDocument implementation from which to work. That will be the subject of this post. In a subsequent post, we will focus on the “naughty bits” related to using UIDocument.

As a quick review, in order to use UIDocument:

  1. You create a class of your own derived from UIDocument.
  2. You implement contentsForType:error: to convert the data you want saved into an instance of NSData or NSFileWrapper.
  3. You implement loadFromContents:ofType:error: to read data that you have saved back in.
  4. To open an instance of your document, you allocate it, initialize it with a file URL pointing to the file to be opened and call openWithCompletionHandler:
  5. To save your document, you can call saveToURL:forSaveOperation:completionHandler:.
  6. When you are finished with the document, you call closeWithCompletionHandler:.

UIDocument can also automatically monitor changes to the document and save it “behind the scenes” for you. The instance has an NSUndoManager that you can use if you support undo and redo, or you can simply call updateChangeCount: when you make modifications to the document. In either case, iOS will periodically check your document and, if it finds you have made changes, it will save it in the background.

Because we aren’t really worried about the specific contents of the document, but more on the save-and-load techniques, we will use a relatively simple data model. Imagine that we are designing an application that’s going to help us remember our friends’ birthdays. Our application’s data model will simply be an array of objects, each of which contains a name and a birthdate. Thus, we might start out with the following as our basic object class:

DDFriend.h:

@interface DDFriend : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSDate *birthdate;
@end

DDFriend.m:

#import "DDFriend.h"

@implementation DDFriend
@synthesize name = _name;
@synthesize birthdate = _birthdate;
@end

A few notes:

  1. I am assuming the use of ARC throughout. Unless you have to support versions of iOS older than 5.0, you definitely want to use ARC.
  2. As you can see from the @synthesize lines, I tend to use a convention of using a trailing underscore character on instance variables. This just helps me keep them separate from method parameters of the same name.

One of the easiest ways of converting objects to and from NSData is to use the NSCoding process. This provides a standard method of serializing and deserializing objects. To do this, change the first line of our object definition to use the NSCoding protocol:

@interface DDFriend : NSObject<NSCoding>

and implement initWithCoder: and encodeWithCoder: in DDFriend.m:

#define kFriendKeyName      @"name"
#define kFriendKeyBirthdate @"birthdate"

- (id) initWithCoder:(NSCoder *)decoder
{
    self = [super init];
    
    if (self)
    {
        _name = [decoder decodeObjectForKey:kFriendKeyName];
        _birthdate = [decoder decodeObjectForKey:kFriendKeyBirthdate];
    }
    
    return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeObject:self.name forKey:kFriendKeyName];
    [encoder encodeObject:self.birthdate forKey:kFriendKeyBirthdate];
}

Now we need to set up our UIDocument-based class.

DDFriendList.h:

@interface DDFriendList : UIDocument
@property (nonatomic, strong) NSMutableArray *friends;
@end

<code>DDFriendList.m</code>:

@implementation DDFriendList
@synthesize friends = _friends;

- (id) initWithFileURL:(NSURL *)url
{
    self = [super initWithFileURL:url];
    if (self)
    {
        _friends = [NSMutableArray array];
    }
    return self;
}
@end

As you can see, it simply contains a mutable array, which will hold our list of DDFriend objects. In a production object we’d probably hide the array and provide a variety of accessors and mutators to manipulate it, but that’s not the point of today’s tutorial, so we’ll take the shortcut of just exposing the array.

Now we’re ready to implement saving and restoring data. To save the data, we can use a NSKeyedArchiver to store the array into an instance of NSMutableData:

#define kFriendListKeyArray @"array"

- (id) contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    
    [archiver encodeObject:_friends forKey:kFriendListKeyArray];
    
    [archiver finishEncoding];
    return data;
}

This encodes the array and all its contents – the individual instances of DDFriend will be encoded using the NSCoding protocol’s encodeWithCoder: method.

To load the data back in, all we do is reverse the process in loadFromContents:ofType:error:

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError
{
    NSData *data = (NSData *)contents;
    
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    
    _friends = [unarchiver decodeObjectForKey:kFriendListKeyArray];
    
    return YES;
}

The contents are passed in as an id because contentsForType:error: could have returned either an instance of NSData or an instance of NSFileWrapper. We know what we wrote, however.

So having written this code, let’s unit test it. (You do write unit tests, don’t you?) In our test project, let’s set up DDFriendListTests.m as follows:


#import <Foundation/Foundation.h>
#import <SenTestingKit/SenTestingKit.h>

@interface DDFriendListTests : SenTestCase
@end

#define kUnitTestFileName   @"DDFriendListTest.dat"

@implementation DDFriendListTests
{
    NSString    * _unitTestFilePath;
    NSURL       * _unitTestFileUrl;
}

- (void)setUp
{
    [super setUp];
    
    NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docsDir = [dirs objectAtIndex:0];
    
    _unitTestFilePath = [docsDir stringByAppendingPathComponent:kUnitTestFileName];
    _unitTestFileUrl = [NSURL fileURLWithPath:_unitTestFilePath];
    
    [[NSFileManager defaultManager] removeItemAtURL:_unitTestFileUrl error:NULL];
}

I have rarely run into a situation in which it’s worth having separate .h and .m files for a unit test, so I typically declare both the interface and implementation in a single file. As you can see, the setUp method, which will be run before every test

  1. Locates the document directory for the app
  2. Creates a path to a unit test document file
  3. Creates a URL to the same file
  4. Makes sure that the file is not present, in case it’s been left over by a previous test

This gives us our basic framework. Now let us test to see if it works. The first test we will implement is that calling the “save” function actually creates a file:

- (void)testSavingCreatesFile
{
    // given that we have an instance of our document
    DDFriendList * objUnderTest = [[DDFriendList alloc] initWithFileURL:_unitTestFileUrl];
    
    // when we call saveToURL:forSaveOperation:completionHandler:
    __block BOOL blockSuccess = NO;
    
    [objUnderTest saveToURL:_unitTestFileUrl
           forSaveOperation:UIDocumentSaveForCreating
          completionHandler:
            ^(BOOL success)
            {
                blockSuccess = success;
            }
     ];
    
    // then the operation should succeed and a file should be created
    STAssertTrue(blockSuccess, nil);
    STAssertTrue([_fileManager fileExistsAtPath:_unitTestFilePath], nil);
}

We create an instance of our class, call the save function, arrange for the completion block to save the “success” parameter where we can get it, and then check that it was successful and that a file was created.

If you run this unit test, you will find it fails. If you stop and think about it, the save process you’ve requested is asynchronous, so you should expect a short delay before the completion handler runs. Here, we’ve charged ahead and started testing results without giving the asynchronous process a chance to run. Your first thought might be to simply inject a delay into the code, or “sleep” the main thread for long enough for the process to complete. That’s actually a fairly fragile technique, but even if you do that, you will find that the test still fails. If you put in a few breakpoints, you will find that contentsForType:error: does get called, but that the completion block never executes. And you’ll scratch your head. A lot.

A slight diversion. The asynchronous process that handles getting your file saved runs on a background thread, using Grand Central Dispatch (GCD), one of the cooler things that Apple introduced in iOS 4.0. However, background threads can be quite evil – if they start messing with the contents of your document while your user interface is doing the same things, Bad Things will happen. As a result, the process works something like this:

  1. When it’s time to save your document, iOS will call contentsForType:error: on the same thread that you called saveToURL:forSaveOperation:completionHandler:. This is almost always the UI thread. Thus, the process of serializing your document won’t cause multi-threading issues with your UI thread.
  2. iOS will go off and do its background thing, writing the data out to the disk, or to iCloud, or wherever. This will be done in the background, on a different thread.
  3. When the operation is complete, whether successful or not, your completion block will get called on the thread that originally called saveToURL:forSaveOperation:completionHandler:. Again, that’s usually your UI thread.

The underlying issue here is that the process of the background thread requesting the UI thread to run your completion block depends on event processing and a run loop. Unit tests do not execute a run loop the way that a “normal” application does – they just, well, run. No run loop, no completion block.

Fortunately, we can execute a run loop ourselves. So modify our unit test class as follows:

@implementation DDFriendListTests
{
    NSFileManager   * _fileManager;
    NSString        * _unitTestFilePath;
    NSURL           * _unitTestFileUrl;
    BOOL              _blockCalled;
}

- (void)setUp
{
    [super setUp];
    
    NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docsDir = [dirs objectAtIndex:0];
    
    _unitTestFilePath = [docsDir stringByAppendingPathComponent:kUnitTestFileName];
    _unitTestFileUrl = [NSURL fileURLWithPath:_unitTestFilePath];
    
    _fileManager = [NSFileManager defaultManager];
    [_fileManager removeItemAtURL:_unitTestFileUrl error:NULL];
    
    _blockCalled = NO;
}

- (void) blockCalled
{
    _blockCalled = YES;
}

- (BOOL) blockCalledWithin:(NSTimeInterval)timeout
{
    NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:timeout];
    while (!_blockCalled && [loopUntil timeIntervalSinceNow] > 0)
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:loopUntil];
    }
    
    BOOL retval = _blockCalled;
    _blockCalled = NO;  // so ready for next time
    return retval;
}

- (void)testSavingCreatesFile
{
    // given that we have an instance of our document
    DDFriendList * objUnderTest = [[DDFriendList alloc] initWithFileURL:_unitTestFileUrl];
    
    // when we call saveToURL:forSaveOperation:completionHandler:
    __block BOOL blockSuccess = NO;
    
    [objUnderTest saveToURL:_unitTestFileUrl
           forSaveOperation:UIDocumentSaveForCreating
          completionHandler:
            ^(BOOL success)
            {
                blockSuccess = success;
                [self blockCalled];
            }
     ];
    
    STAssertTrue([self blockCalledWithin:10], nil);
    
    // then the operation should succeed and a file should be created
    STAssertTrue(blockSuccess, nil);
    STAssertTrue([_fileManager fileExistsAtPath:_unitTestFilePath], nil);
}

blockCalledWithin: executes a default run loop until either blockCalled is called, or until a timeout occurs, returning YES in the former case and NO in the latter. With these changes, all the back-and-forth required happens as it should, and our unit test now passes.

Now let’s write a unit test to make sure that the data loads back properly. Here we’ll create an object, put some data in it, save it to a file, close it and then load the data back from that file, using the same techniques as before:

- (void) testLoadingRetrievesData
{
    // given that we have saved the data from an instance of our class
    NSDate *birthdate = [NSDate date];
    DDFriend *friend = [[DDFriend alloc] init];
    friend.name = @"Me";
    friend.birthdate = birthdate;
    
    DDFriendList * document = [[DDFriendList alloc] initWithFileURL:_unitTestFileUrl];
    [document.friends addObject:friend];
    
    __block BOOL blockSuccess = NO;
    
    [document saveToURL:_unitTestFileUrl
           forSaveOperation:UIDocumentSaveForCreating
          completionHandler:
             ^(BOOL success)
             {
                 blockSuccess = success;
                 [self blockCalled];
             }
     ];
    
    STAssertTrue([self blockCalledWithin:10], nil);
    STAssertTrue(blockSuccess, nil);
    
    [document closeWithCompletionHandler:
         ^(BOOL success)
         {
             blockSuccess = success;
             [self blockCalled];
         }
     ];
    
    STAssertTrue([self blockCalledWithin:10], nil);
    STAssertTrue(blockSuccess, nil);
    
    // when we load a new document from that file
    DDFriendList * objUnderTest = [[DDFriendList alloc] initWithFileURL:_unitTestFileUrl];
    [objUnderTest openWithCompletionHandler:
         ^(BOOL success)
         {
             blockSuccess = success;
             [self blockCalled];
         }
     ];
    
    // the data should load successfully and be what we saved
    
    STAssertTrue([self blockCalledWithin:10], nil);
    STAssertTrue(blockSuccess, nil);
    
    NSArray * friends = objUnderTest.friends;
    STAssertEquals([friends count], (NSUInteger)1, nil);
    DDFriend *restoredFriend = [friends objectAtIndex:0];
    STAssertEqualObjects(restoredFriend.name, @"Me", nil);
    STAssertEqualObjects(restoredFriend.birthdate, birthdate, nil);
}

Again, this unit test works, indicating that our save and load code play nicely together, so we have a working base on which to continue.

This concludes the first part of this tutorial. In the second part of the tutorial, we will start exploring the various things that can go wrong, and how we can improve our UIDocument-based class to compensate.

You can download the code for this tutorial from https://github.com/SilverBayTech/Defensive-UIDocument.