samvermette.com

FEB 5, 2012

Getting started with iCloud Storage

iCloud being constantly advertised as “it just works”, one would think it’s just as easy to implement into an app. Unfortunately, Apple doesn’t seem to have put as much attention into the documentation as it did into the inner working of the APIs. After spending a whole night cycling between Xcode, Google and StackOverflow, I finally managed to get this iCloud storage thing working up nicely.

(This article only deals with moving files to iCloud Storage using NSFileManager. There is already a lot of great documentation about the key-value store and the UI/NSDocument APIs).

Setting up our iCloud-enabled app

NOTE: iCloud doesn’t work in iOS Simulator. You’ll need to test/debug iCloud code directly on your device (fun times).

1. Creating an iCloud-enabled provisioning profile

Since we can only run this on a physical device, we’re going to need to hit the Provisioning Portal to create an app ID enabled for iCloud, and download a provisioning profile signed for that app ID.

2. Setting your iCloud container identifiers

Next, we need to set our app’s iCloud container identifiers in Xcode. Do that by hitting the app’s target and then in “Summary” scroll down to “Entitlements”. In “iCloud Containers”, enter the bundle identifier of the app ID you just created.

(The documentation says to also input our Team Prefix in there, but if we look at the auto-generated MyApp.entitlements, we see that it’s automatically added by Xcode on compile)

Interacting with iCloud

iCloud storage primarily revolves around 2 classes: NSFileManager (that you’re probably already familiar with) and NSMetadataQuery. The former lets us move files to and from the iCloud sandbox (which is ultimately just another directory on the device that iOS uses to track what should get uploaded to iCloud). The latter allows us to retrieve the listing of those files, regardless of whether they have been downloaded yet or not.

1. Getting your iCloud sandbox URL

To make sure iCloud is enabled and functioning properly, first we need to get our app’s ubiquity container URL (i.e. its iCloud sandbox URL):

self.ubiquitousURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

Passing in nil as the container identifier parameter tells NSFileManager to use the identifier provided in MyApp.entitlements. If that call returns nil, it means iCloud is not enabled for the current user or device (for instance if we’re running it in the Simulator). It’s a good idea to keep a strong reference to this URL since we’re going to use it a lot in interacting with iCloud.

2. Staying in touch with the sandbox’s content

That’s where NSMetadataQuery comes in. Here’s how to set it up:

self.metadataQuery = [[NSMetadataQuery alloc] init];
[metadataQuery setPredicate:[NSPredicate predicateWithFormat:@"%K LIKE '*'", NSMetadataItemFSNameKey]];

Again, we’re keeping a strong reference to this object since we need it to stay alive for as long as the app lives. NSMetadataQuery requires a predicate to function, but for the sake of this example we’re just giving it a dummy match-all predicate so it returns all files in our sandbox. In addition to predicates, filtering can be done using setSearchScope:.

To get notified of changes made to the sandbox’s content, we need to register our metadata query with NSNotificationCenter:

[[NSNotificationCenter defaultCenter] addObserver:self 
                                         selector:@selector(queryDidReceiveNotification:) 
                                             name:NSMetadataQueryDidUpdateNotification 
                                           object:self.metadataQuery];

NSMetadataQuery also offers didFinish, didStart and gatheringProgress notifications but we’re just going to register didUpdate to keep it simple.

When our metadata query sends a didUpdate notification, we’re just going to log our NSMetadataItems’ info so we know it works (in an actual app though this is where we would update our UI):

- (void)queryDidReceiveNotification:(NSNotification *)notification {
    NSArray *results = [self.metadataQuery results];
    
    for(NSMetadataItem *item in results) {
        NSString *filename = [item valueForAttribute:NSMetadataItemDisplayNameKey];
        NSNumber *filesize = [item valueForAttribute:NSMetadataItemFSSizeKey]; 
        NSDate *updated = [item valueForAttribute:NSMetadataItemFSContentChangeDateKey];
        NSLog(@"%@ (%@ bytes, updated %@)", filename, filesize, updated);
    }
}

Now that our NSMetadataQuery is set up and tied with NSNotificationCenter, it’s time to fire it up:

[self.metadataQuery startQuery];

This call will return nil if the query fails to start (because of a missing predicate, for instance).

3. Moving content to and from iCloud Storage

In practice, uploading a file to iCloud is really nothing more than moving it to another directory (the iCloud sandbox). However, since that directory lives outside of our ordinary app sandbox, Apple added a special method to NSFileManager that grants us the permission to move files to it:

NSURL *destinationURL = [self.ubiquitousURL URLByAppendingPathComponent:@"Documents/image.jpg"]
[[NSFileManager defaultManager] setUbiquitous:YES 
                                    itemAtURL:sourceURL
                               destinationURL:destinationURL
                                        error:&error]

If that call is successful (i.e. returns YES), iOS will automatically take charge of the actual uploading and everything that ensues. By the time the file is done uploading, our NSMetadataQuery will already have posted a NSMetadataQueryDidUpdateNotification notification (remember, iCloud Storage is all about meta data).

In practice, making this call seems like the easiest part of it all. But there are a few things (some of them undocumented) that you need to know to get it to work properly:


hello@samvermette.com