So you would like to find out how can you make use of iCloud? Fair enough. Cloud computing got quite popular recently and it seems that marketing departments from all over the world fell in love with it. Despite promoting it as the latest n’ greatest thing eva™, cloud computing is nothing new. Using a mainframe by the means of dumb terminal, projects such as SETI or just plain e-mail can also be categorized as cloud services and the have been around for a while now.
Ok, but we are not going to talk about that. Our subject is iCloud. You know iCloud, you’ve used it, however, iCloud is more of an umbrela term for storage and services provided by Apple as a replacement for MobileMe than a single entity. Following text is going to be about utilizing storage provided by iCloud.
Take note that although we aren’t going to cover every detail, all the most important information that is common for all the uses of iCloud is here.
Almost everything here is common for OS X and iOS. When it’s not the case, I’ll highlight it.
iCloud is available for OS X since 10.7 and iOS since iOS 5. There are small but significant differences between iOS 5 & 6 and OS X 10.7 & 10.8 which we will cover along the way.
There are four distinct ways of using iCloud:
- Key-Value storage: It is basically NSUserDefaults in the cloud. Suitable for small amount of information. I haven’t used it myself so I am not going to talk about it. I guess that it works alright as it is fairly simple. For more information visit Designing for Key-Value Data in iCloud
- Document storage: It stores UIDocument and NSDocument. You can read about it here. I don’t care about it for the time being.
- Core Data storage: To save you lots of pain, just stay away from it. Do it. This is an amazing idea that you put your core data in cloud and magically it gets synchronized, merged and amazing on all of your devices. Unfortunately, it doesn’t work. I’ve tried it, got random errors without any meaningful feedback, wasted lot of time to figure it out and then gave up. If you don’t believe me, check this, this, this and this. Just let it die. If you don’t, this will happen.
- Files storage: So this is finally the subject we will focus on. From bird’s eye view it works as a folder, which, if you put anything into it, magically gets sychronized into all of your iCloud enabled devices. Unfortunatelly, Apple doesn’t like to share too much information as it supposed to “just work”. And that’d be great, but some of those infos, we DO NEED. That’s why you’re here, so you don’t have to waste time on Apple’s docs which are just not explanatory enough.
Despite being “in the magical cloud in the sky”, you can only access files that you have physically on your device. On OS X cloud files are stored in ~/Library/Mobile Documents/. If you go there, you’ll notice that it’s kinda special. On staus bar it shows that you’re in folder called iCloud (which is not true), a cloud icon is used to represent it when you’re inside, ⌘+↑ shortcut to go up in hierarchy doesn’t work and when you want to delete something, a specific warning shows up that you’re about to upset Apple.
To see which files are really up in the magical cloud you use iCloud Developer. Very usefull. I like that the shining effect rotates when you move your mouse. That’s what we all like about Apple, those tiny details that every other company doesn’t give a fudge about.
So how do I use it?
First, you need to enable iCloud for your App ID in the Certificates, Identifiers & Profiles portal.
Then you should get yourself a pair of provisioning profiles. Wildcards won’t cut it, so you need separate one for development and one for distribution.
In your Xcode project, go to your target and in summary tab enable “Use Entitlements File”, then check “Enable iCloud”.
Don’t forget to make sure that your app is being built using correct provisioning profile.
Before we start sending data all over the place, we need to sort out our iCloud session. The things we have to be able to do:
- Check if user is logged into iCloud
- Check if it is the same user as the last time
- Be notified when user logs out or different user logs in
The class we’ll be using is NSFileManager.
[[NSFileManager defaultManager] ubiquityIdentityToken];
It will provide us with token of type id. Doesn’t matter what it is underneath (I don’t care). What matters is that you can run isEqual and see if it has changed since the last time. Preferably you will store this in user defaults (or some other place) so upon start of app you can make it behave accordingly. Upon start, there are four posibilities:
- This is the first run of the app, so we don’t have past token
- It’s been run before and the token is the same as the last time
- It’s different token, so do something about it
- User isn’t logged in because the token is nil
How it’s handled is up to you.
It’s all great and dandy, but Apple wouldn’t be itself if it didn’t mess something up. ubiquityIdentityToken isn’t available on iOS 5 and OS X 10.7. Ah, fudge you Apple. So what do we do?
[[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
This thing returns us with NSURL to location of our app’s iCloud storage. How can we use it to identify users? Well, what I did is:
NSURL *cloudIdUrl; //Points to a file in app's icloud storage
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
NSString *currentCloudId = CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid));
currentCloudId = [currentCloudId copy];
[(NSString *)currentCloudId writeToFile:cloudIdUrl
So I’m storing a GUID as text in some file in our app’s iCloud folder. In this way I just have to read the contents of that file and compare it with the last one:
- There is no token file in our iCloud storage, so it’s the first run
- It’s the same as in the last run
- It’s different than than in the last run
- URLForUbiquityContainerIdentifier returned nil, so user isn’t logged in
Great, so now we can identify user, but there is one more thing (ha!). According to Apple’s docs about URLForUbiquityContainerIdentifier method:
Important: Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread. To determine if iCloud is available, especially at launch time, call the
ubiquityIdentityToken method instead.
Specific as always. So what we need to do is run iCloud setup in background thread, otherwise we might hog up the UI.
And the last thing is now observing changes to the iCloud session. This notifies us that something about iCloud session has changed:
[[NSNotificationCenter defaultCenter] addObserver:self
When your selector gets called, you basically want to execute all of the setup over again.
Uff. So that’s a bit of work for something that “just works”. Alright, now that we got over with all the boring stuff, let’s get down to business.
We don’t have direct control over data transmission in iCloud. So if you want to upload something, you can only write file to your mobile documents folder and observer what’s happening.
So, how do we check what’s happening under the hood of iCloud? With the use of NSMetadataQuery of course! So here is a pice of code that will call cloudDataChanged whenever there have been changes to notes.xml file:
NSMetadataQuery *notesQuery = [[NSMetadataQuery alloc] init];
notesQuery.predicate = [NSPredicate predicateWithFormat:@"%K == 'notes.xml'", NSMetadataItemFSNameKey];
notesQuery.searchScopes = @[NSMetadataQueryUbiquitousDocumentsScope];
[[NSNotificationCenter defaultCenter] addObserver:self
So now we’ll get informed whenever something’s happened, but how do we check what’s happened? We check that with NSMetadataItem that we extract from the notesQuery (that’s why it’s passed as object for observer, but you could as well have it as property). So inside cloudDataChanged: (or however you’ve named your method) do something like this:
- (void)cloudDataChanged:(NSNotification *)notification_
NSMetadataItem *metadataItem = (NSMetadataItem *)[[notification_ object] resultAtIndex:0];
BOOL isDownloaded = [[metadataItem valueForAttribute:NSMetadataUbiquitousItemIsDownloadedKey] boolValue];
BOOL isDownloading = [[metadataItem valueForAttribute:NSMetadataUbiquitousItemIsDownloadingKey] boolValue];
BOOL isUploaded = [[metadataItem valueForAttribute:NSMetadataUbiquitousItemIsUploadedKey] boolValue];
BOOL isUploading = [[metadataItem valueForAttribute:NSMetadataUbiquitousItemIsUploadingKey] boolValue];
There is a bit of trickery with those flags, because our cloudDataChanged: can get called a few times before upload or download is complete, with different flags set each time. How do they change? Well, it’s not really specified and with a bit of experimentation by myself, I can tell you that the only thing we can be sure about is that when download or upload is finished, isDownloaded & isUploaded will be YES. So how do we distinguish between them? It’s a tricky thing. I did it, is as follows – I know when I want to upload something (because I have to write a file myself into mobile documents folder) so when I do it, I a set a flag which indicates whether I’m uploading or downloading. This way we just need to make sure that this flag is handled correctly. It works well, but seems very hackish. If you know a better way, let me know.
NSMetadataItem also has a few other flags. Most interesting ones are NSMetadataUbiquitousItemPercentDownloadedKey and NSMetadataUbiquitousItemPercentUploadedKey which should be self explanatory.
The last piece of the puzzle has to happen before you start opening any files from iCloud. First, check is desired file exists:
NSURL *cloudUrl; //Correctly initialized URL;
[[NSFileManager defaultManager] fileExistsAtPath:cloudUrl]
Here comes a twist, because if you just check if file exists and then proceed with opening it, it may fail. Why? Because on iOS files from iCloud are not by default downloaded (on OS X they are, but we cannot bet on this). With this knowledge, we have to to check if the file is downloaded, like this:
[cloudUrl getResourceValue:&isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:&error];
And depending on the result, we can download the file using:
[[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:cloudUrl error:&error];
The status of our download is available thanks to NSMetadataQuery that we’ve talked about before. Because the way it is setup isn’t very convenient for use, I recommend that you wrap downloading into some sexy method. I’ve utilized such idea:
- (void)downloadCloudFileAtUrl:(NSURL *)url_ downloadFinished:(void (^)())block_;
You say which file you want to download and what to do when it’s downloaded. Inside it checks if it’s already downloaded and if it is, the the block is immediately executed. Otherwise, download of the file is requested and once isDownloaded and isUploaded flags are both YES inside cloudDataChanged: notification handler, the block is executed. But feel free to implement (and share with us!) a better idea.
Sum it up!
To conclude all that has been said, here is a typical workflow in which you open an iCloud file, make changes and save it:
- Setup observers for iCloud session (NSUbiquityIdentityDidChangeNotification)
- Check if user is logged in and if it’s the same or different user than the last time and behave accordingly (taking into account differences between OS X 10.7 iOS 5 and OS X 10.8 iOS 6)
- Setup observers for iCloud files with NSMetadataQuery
- Check if youre lovely file needs a download, download it and once it’s done, do things to it
- Save the file just like would you any other, do stuff once it has been uploaded or simply ignore it
- Go for a beer
Do you want iCloud at all?
This may sound like a stupid question, but really, think about it. iCloud is great as you can assume that every Mac or iOS user will have an iCloud account, it integrates nicely with system and is as if it wasn’t, but being an Apple service you’ll have hard time predicting it’s reliability (or even predict all the issues that future versions of iOS and OS X may cause). Even Apple has issues with it, so what makes you think you won’t? And if you want an universal cloud service for Android, Windows and your toaster, you’re out of luck with iCloud.
You always have other options to consider like Dropbox, SkyDrive, or creating your own service (quite a few apps do that).
Nevertheless, I have high hopes for iCloud and am sure that Apple is working hard to fix all the issues and keep the promises. Cloud service is not an easy thing to get right, you know.
Lemme know what you think and share important things that I might have left out.