Beginning iCloud in iOS 5 Tutorial Part 1

Update 10/24/12: If you’d like a new version of this tutorial fully updated for iOS 6 and Xcode 4.5, check out iOS 5 by Tutorials Second Edition! Note from Ray: This is the ninth iOS 5 tutorial in the iOS 5 Feast! This tutorial is a free preview chapter from our new book iOS 5 […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Subclassing UIDocument

Now that you have a good overview of UIDocument, let’s create a subclass for our note application and see how it works!

Create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Note, and make it a subclass of UIDocument.

To keep things simple, our class will just have a single property to store the note as a string. To add this, replace the contents of Note.h with the following:

#import <UIKit/UIKit.h>

@interface Note : UIDocument

@property (strong) NSString * noteContent;

@end

As we have learned above we have two override points, one when we read and one when we write. Add the implementation of these by replacing Note.m with the following:


#import "Note.h"

@implementation Note

@synthesize noteContent;

// Called whenever the application reads data from the file system
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName 
  error:(NSError **)outError
{
    
    if ([contents length] > 0) {
        self.noteContent = [[NSString alloc] 
            initWithBytes:[contents bytes] 
            length:[contents length] 
            encoding:NSUTF8StringEncoding];        
    } else {
        // When the note is first created, assign some default content
        self.noteContent = @"Empty"; 
    }
    
    return YES;    
}

// Called whenever the application (auto)saves the content of a note
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError 
{
    
    if ([self.noteContent length] == 0) {
        self.noteContent = @"Empty";
    }
    
    return [NSData dataWithBytes:[self.noteContent UTF8String] 
        length:[self.noteContent length]];
    
}

@end

When we load a file we need a procedure to ‘transform’ the NSData contents returned by the background queue into a string. Conversely, when we save we have to encode our string into an NSData object. In both cases we do a quick check and assign a default value in case the string is empty. This happens the first time that the document is created.

Believe it or not, the code we need to model the document is already over! Now we can move to the code related to loading and updating.

Opening an iCloud File

First of all we should decide a file name for our document. For this tutorial, we’ll start by creating a single filename. Add the following #define at the top of AppDelegate.m:

#define kFILENAME @"mydocument.dox"

Next, let’s extend the application delegate to keep track of our document, and a metadata query to look up the document in iCloud. Modify AppDelegate.h to look like the following:

#import <UIKit/UIKit.h>
#import "Note.h"

@class ViewController;

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@property (strong) Note * doc;
@property (strong) NSMetadataQuery *query;

- (void)loadDocument;

@end

Then switch to AppDelegate.m and synthesize the new propeties:

@synthesize doc = _doc;
@synthesize query = _query;

We’ve already added code into application:didFinishLaunchingWithOptions to check for the availability of iCloud. If iCloud is available, we want to call the new method we’re about to write to load our document from iCloud, so add the following line of code right after where it says “TODO: Load document”:

[self loadDocument];

Next we’ll write the loadDocument method. Let’s put it together bit by bit so we can discuss all the code as we go.

- (void)loadDocument {
    
    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query; 
    
}

Note that before we can load a document from iCloud, we first have to check what’s there. Remember that we can’t simply enumerate the local directory returned to us by URLForUbiquityContainerIdentifier, because there may be files in iCloud not yet pulled down locally.

If you ever worked with Spotlight on the Mac, you’ll be familiar with the class NSMetadataQuery. It is a class to represent results of a query related to the properties of an object, such as a file.

In building such a query you have the possibility to specify parameters and scope, i.e. what you are looking for and where. In the case of iCloud files the scope is always NSMetadataQueryUbiquitousDocumentsScope. You can have multiple scopes, so we have to build an array containing just one item.

So continue loadDocument as follows:

- (void)loadDocument {
    
    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query; 
    [query setSearchScopes:[NSArray arrayWithObject:
        NSMetadataQueryUbiquitousDocumentsScope]];
    
}

Now you can provide the parameters of the query. If you ever worked with CoreData or even arrays you probably know the approach. Basically, you build a predicate and set it as parameter of a query/search.

In our case we are looking for a file with a particular name, so the keyword is NSMetadataItemFSNameKey, where ‘FS’ stands for file system. Add the code to create and set the predicate next:

- (void)loadDocument {
    
    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;
    [query setSearchScopes:[NSArray arrayWithObject:
        NSMetadataQueryUbiquitousDocumentsScope]];
    NSPredicate *pred = [NSPredicate predicateWithFormat:
        @"%K == %@", NSMetadataItemFSNameKey, kFILENAME];
    [query setPredicate:pred];
    
}

You might not have seen the %K substitution before. It turns out predicates treat formatting characters a bit differently than you might be used to with NSString’s stringWithFormat. When you use %@ in predicates, it wraps the value you provide in quotes. You don’t want this for keypaths, so you use %K instead to avoid wrapping it in quotes. For more information, see the Predicate Format String Syntax in Apple’s documentation.

Now the query is ready to be run, but since it is an asynchronous process we need to set up an observer to catch a notification when it completes.

The specific notification we are interested in has a pretty long (but descriptive) name: NSMetadataQueryDidFinishGatheringNotification. This is posted when the query has finished gathering info from iCloud.

So here is the final implementation of our method:

- (void)loadDocument {
    
    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;
    [query setSearchScopes:[NSArray arrayWithObject:
        NSMetadataQueryUbiquitousDocumentsScope]];
    NSPredicate *pred = [NSPredicate predicateWithFormat: 
        @"%K == %@", NSMetadataItemFSNameKey, kFILENAME];
    [query setPredicate:pred];
    [[NSNotificationCenter defaultCenter] 
        addObserver:self 
        selector:@selector(queryDidFinishGathering:) 
        name:NSMetadataQueryDidFinishGatheringNotification 
        object:query];
    
    [query startQuery];
    
}

Now that this is in place, add the code for the method that will be called when the query completes:

- (void)queryDidFinishGathering:(NSNotification *)notification {
    
    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];
    
    [[NSNotificationCenter defaultCenter] removeObserver:self 
        name:NSMetadataQueryDidFinishGatheringNotification
        object:query];
    
    _query = nil;
    
	[self loadData:query];
    
}

Note that once you run a query, if you don’t stop it it runs forever or until you quit the application. Especially in a cloud environment things can change often. It might happen that while you are processing the results of a query, due to live updates, the results change! So it is important to stop this process by calling disableUpdates and stopQuery. In particular the first prevents live updates and the second allows you to stop a process without deleting already collected results.

We then remove ourselves as an observer to ignore further notifications, and finally call a method to load the document, passing the NSMetadataQuery as a parameter.

Add the starter implementation of this method next (add this above queryDidFinishGathering):

- (void)loadData:(NSMetadataQuery *)query {
    
    if ([query resultCount] == 1) {
        NSMetadataItem *item = [query resultAtIndex:0];
        
	}
}

As you can see here, a NSMetadataQuery wraps an array of NSMetadataItems which contain the results. In our case, we are working with just one file so we are just interested in the first element.

An NSMetadataItem is like a dictionary, storing keys and values. It has a set of predefined keys that you can use to look up information about each file:

  • NSMetadataItemURLKey
  • NSMetadataItemFSNameKey
  • NSMetadataItemDisplayNameKey
  • NSMetadataItemIsUbiquitousKey
  • NSMetadataUbiquitousItemHasUnresolvedConflictsKey
  • NSMetadataUbiquitousItemIsDownloadedKey
  • NSMetadataUbiquitousItemIsDownloadingKey
  • NSMetadataUbiquitousItemIsUploadedKey
  • NSMetadataUbiquitousItemIsUploadingKey
  • NSMetadataUbiquitousItemPercentDownloadedKey
  • NSMetadataUbiquitousItemPercentUploadedKey

In our case, we are interested in NSMetadataItemURLKey, which points to the URL that we need to build our Note instance. Continue the loadData method as follows:

- (void)loadData:(NSMetadataQuery *)query {
    
    if ([query resultCount] == 1) {
        
        NSMetadataItem *item = [query resultAtIndex:0];
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
        Note *doc = [[Note alloc] initWithFileURL:url];
        self.doc = doc;
		
	}
}

When you create a UIDocument (or a subclass of UIDocument like Note), you always have to use the initWithFileURL initializer and give it the URL of the document to open. We call that here, pasing in the URL of the located file, and store it away in an instance variable.

Now we are ready to open the note. As explained previously you can open a document with the openWithCompletionHandler method, so continue loadData as follows:


- (void)loadData:(NSMetadataQuery *)query {
    
    if ([query resultCount] == 1) {
        
        NSMetadataItem *item = [query resultAtIndex:0];
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
        Note *doc = [[Note alloc] initWithFileURL:url];
        self.doc = doc;
        [self.doc openWithCompletionHandler:^(BOOL success) {
            if (success) {                
                NSLog(@"iCloud document opened");                    
            } else {                
                NSLog(@"failed opening document from iCloud");                
            }
        }];

	}
}

You can run the app now, and it seems to work… except it never prints out either of the above messages! This is because there is currently no document in our container in iCloud, so the search isn’t finding anything (and the result count is 0).

Since the only way to add a document on the iCloud is via an app, we need to write some code to create a doc. We will append this to the loadData method that we defined a few seconds ago. When the query returns zero results, we should:

  • Retrieve the local iCloud directory
  • Initialize an instance of document in that directory
  • Call the saveToURL method
  • When the save is successful we can call openWithCompletionHandler.

So add an else case to the if statement in loadData as follows:

else {
    
    NSURL *ubiq = [[NSFileManager defaultManager] 
      URLForUbiquityContainerIdentifier:nil];
    NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:
      @"Documents"] URLByAppendingPathComponent:kFILENAME];
    
    Note *doc = [[Note alloc] initWithFileURL:ubiquitousPackage];
    self.doc = doc;
    
    [doc saveToURL:[doc fileURL] 
      forSaveOperation:UIDocumentSaveForCreating 
      completionHandler:^(BOOL success) {            
        if (success) {
            [doc openWithCompletionHandler:^(BOOL success) {                
                NSLog(@"new document opened from iCloud");                
            }];                
        }
    }];
}

Compile and run your app, and you should see the “new document” message arrive the first time you run it, and “iCloud document opened” in subsequent runs.

You can even try this on a second device (I recommend temporarily commenting out the else case first though to avoid creating two documents due to timing issues), and you should see the “iCloud document opened” message show up on the second device (because the document already exists on iCloud now!)

Now our application is almost ready. The iCloud part is over, and we just need to set up the UI – which we’ll continue in the next part of the series!

Contributors

Over 300 content creators. Join our team.