Introduction to iCloud Overview The iCloud storage API in iOS 5 allows applications to save user documents and application-specific data to a central location and access those items from all the user's devices. There are four types of storage available: Key-Value storage - to share small amounts of data with your application on a user's other devices. UIDocument storage - to store documents and other data in the user's iCloud account using a subclass of UIDocument. CoreData - SQLite database storage. Individual files and directories - for managing lots of different files directly in the file system. This document discusses the first two types - Key-Value pairs and UIDocument subclasses - and how to use those features in Xamarin.iOS. Requirements Using Xamarin.iOS with iOS 5 requires XCode 4.2 and the iOS 5 SDK from Apple, with Xamarin.iOS 5.0 and either Xamarin Studio 2.8 or Visual Studio 2010 and newer. Preparing for iCloud development Applications must be configured to use iCloud both in the Apple Provisioning Portal and the project itself. Before developing for iCloud (or trying out the samples) follow the steps below. To correctly configure an application to access iCloud: Find your TeamID - login to developer.apple.com and visit the Member Center > Your Account > Developer Account Summary to get your Team ID (or Individual ID for single developers). It will be a 10 character string ( A93A5CM278 for example) - this forms part of the "container identifier". Create a new App ID - To create an App ID, follow the steps outlined in the Provisioning for Store Technologies section of the Device Provisioning guide , and be sure to check iCloud as an allowed Service: [ ](Images/icloud.png) Create a new Provisioning Profile - To create a Provisioning Profile, follow the steps outlined in the Provisioning for Store Technologies section of the Device Provisioning guide . Add your "container identifier" to Entitlements.plist - the container identifier format is TeamID.BundleID, so using the examples above would result in A93A5CM278.com.xamarin.samples.icloud. You must use your own TeamID. You can also use the $(TeamIdentifierPrefix) and $(CFBundleIdentifier) placeholders in Xamarin Studio's Project Options dialog. Add your container identifier to the sample's Entitlements.plist under the following two keys: com.apple.developer.ubiquity-kvstore-identifier com.apple.developer.ubiquity-container-identifiers This will need to be done manually in Visual Studio, where no advanced plist editor is available. Configure Xamarin Studio project properties - open the sample project's Options to ensure the iOS Application Identifier is set to the correct Bundle ID and the iOS Bundle Signing has the correct Provisioning Profile and Custom Entitlements file selected. This can all be done in Visual Studio under the project Properties pane. Enable iCloud on your device - go to Settings > iCloud and ensure that the device is logged in. Select and turn on the Documents & Data option. You must use a device to test iCloud - it will not work on the Simulator. In fact, you really need two or more devices all signed in with the same Apple ID to see iCloud in action. Xamarin Studio Entitlements Editor The latest version of Xamarin Studio now includes an editing UI for the Entitlements.plist file. It will automatically add the $(TeamPrefixIdentifier) to the plist (without showing it), and you can enter the $(CFBundleIdentifier) placeholder as shown: [ ](Images/entitlements.png) This will have to be done manually in Visual Studio. These placeholders are substituted for the correct values during the build, so you don't have to set them manually. Key-Value Storage Key-value storage is intended for small amounts of data that a user might like persisted across devices - such as the last page they viewed in a book or magazine. Key-value storage should not be used for backing-up data. There are some limitations to be aware of when using key-value storage: Maximum key size - Key names cannot be longer than 64 bytes. Maximum value size - You cannot store more than 64 kilobytes in a single value. Maximum key-value store size for an app - Applications can only store up to 64 kilobytes of key-value data in total. Attempts to set keys beyond that limit will fail and the previous value will persist. Data types - Only basic types like strings, numbers and booleans can be stored. The iCloudKeyValue example demonstrates how it works. The sample code creates a key named for each device: you can set this key on one device and watch the value get propagated to others. It also creates a key called "Shared" which can be edited on any device - if you edit on many devices at once, iCloud will decide which value "wins" (using a timestamp on the change) and gets propagated. This screenshot shows the sample in use. When change notifications are received from iCloud they are printed in the scrolling text view at the bottom of the screen and updated in the input fields. Setting and retrieving data This code shows how to set a string value. var store = NSUbiquitousKeyValueStore.DefaultStore; store.SetString("testkey", "VALUE IN THE CLOUD"); // key and value store.Synchronize(); Calling Synchronize ensures the value is persisted to local disk storage only. The synchronization to iCloud happens in the background and cannot be "forced" by application code. With good network connectivity the synchronization will often happen within 5 seconds, however if the network is poor (or disconnected) an update may take much longer. You can retrieve a value with this code: var store = NSUbiquitousKeyValueStore.DefaultStore; display.Text = store.GetString("testkey"); The value is retrieved from the local data store - this method does not attempt to contact iCloud servers to get the "latest" value. iCloud will update the local data store according to its own schedule. Deleting Data To completely remove a key-value pair, use the Remove method like this: var store = NSUbiquitousKeyValueStore.DefaultStore; store.Remove("testkey"); store.Synchronize(); Observing Changes An application can also receive notifications when values are changed by iCloud by adding an observer to the NSNotificationCenter.DefaultCenter. The following code from KeyValueViewController.cs ViewWillAppear method shows how to listen for those notifications and create a list of which keys have been changed: keyValueNotification = NSNotificationCenter.DefaultCenter.AddObserver ( NSUbiquitousKeyValueStore.DidChangeExternallyNotification , delegate (NSNotification n) { NSDictionary userInfo = n.UserInfo; NSNumber reasonNumber = (NSNumber)userInfo.ObjectForKey(NSUbiquitousKeyValueStore.ChangeReasonKey); int reason = reasonNumber.IntValue; // reason change was triggered NSArray changedKeys = (NSArray)userInfo.ObjectForKey (NSUbiquitousKeyValueStore.ChangedKeysKey); var changedKeysList = new List<string> (); for (uint i = 0; i < changedKeys.Count; i++) { var key = new NSString (changedKeys.ValueAt(i)); // resolve key to a string changedKeysList.Add (key); } // now do something with the list... }); Your code can then take some action with the list of changed keys, such as updating a local copy of them or updating the UI with the new values. Possible change reasons are: ServerChange (0), InitialSyncChange (1), or QuotaViolationChange (2). You can access the reason and perform different processing if required (for example, you might need to remove some keys as a result of a QuotaViolationChange). Document Storage iCloud Document Storage is designed to manage data that is important to your app (and to the user). It can be used to manage files and other data that your app needs to run, while at the same time providing iCloud-based backup and sharing functionality across all the user's devices. This diagram shows how it all fits together. Each device has data saved on local storage (the UbiquityContainer) and the operating system's iCloud Daemon takes care of sending and receiving data in the cloud. All file access to the UbiquityContainer must be done via FilePresenter/FileCoordinator to prevent concurrent access. The UIDocument class implements those for you; this example shows how to use UIDocument. The iCloudUIDoc example implements a simple UIDocument subclass that contains a single text field. The text is rendered in a UITextView and edits are propogated by iCloud to other devices with a notification message shown in red. The sample code does not deal with more advanced iCloud features like conflict resolution. This screenshot shows the sample application - after changing the text and pressing UpdateChangeCount the document is synchronized via iCloud to other devices. There are five parts to the iCloudUIDoc sample: Accessing the UbiquityContainer - determine if iCloud is enabled, and if so the path to your application's iCloud storage area. Creating a UIDocument subclass - create a class to intermediate between iCloud storage and your model objects. Finding and opening iCloud documents - use * NSFileManager* and * NSPredicate* to find iCloud documents and open them. Displaying iCloud documents - expose properties from your * UIDocument* so that you can interact with UI controls. Saving iCloud documents - ensure that changes made in the UI are persisted to disk and iCloud. All iCloud operations run (or should run) asynchronously so that they don't block while waiting for something to happen. You will see three different ways of accomplishing this in the sample: Threads - in * AppDelegate.FinishedLaunching* the initial call to * GetUrlForUbiquityContainer* is done on another thread to prevent blocking the main thread. NotificationCenter - registering for notifications when asynchronous operations such as * NSMetadataQuery.StartQuery* complete. Completion Handlers - passing in methods to run on completion of asynchronous operations like * UIDocument.Open*. Accessing the UbiquityContainer The first step in using iCloud Document Storage is to determine whether iCloud is enabled, and if so the location of the "ubiquity container" (the directory where iCloud-enabled files are stored on the device). This code is in the AppDelegate.FinishedLaunching method of the sample. // GetUrlForUbiquityContainer is blocking, Apple recommends background thread new Thread(new ThreadStart(() => { var uburl = NSFileManager.DefaultManager.GetUrlForUbiquityContainer(null); // OR instead of null you can specify "TEAMID.com.xamarin.samples.icloud" if (uburl == null) { InvokeOnMainThread(()=> { var alert = new UIAlertView("No \uE049 available" , "Check your Entitlements.plist, BundleId, TeamId and Provisioning Profile!" , null, "OK", null); alert.Show (); }); } else { // iCloud enabled, store the NSURL for later use iCloudUrl = uburl; } })).Start(); Although the sample does not do so, Apple recommends calling GetUrlForUbiquityContainer whenever an app comes to the foreground. Creating a UIDocument Subclass All iCloud files and directories (ie. anything stored in the UbiquityContainer directory) must be managed using NSFileManager methods, implementing the NSFilePresenter protocol and writing via an NSFileCoordinator. The simplest way to do all of that is not to write it yourself, but subclass UIDocument which does it all for you. There are only two methods that you must implement in a UIDocument subclass to work with iCloud: LoadFromContents - passes in the NSData of the file's contents for you to unpack into your model class/es. ContentsForType - request for you to supply the NSData representation of your model class/es to save to disk (and the Cloud). This sample code from iCloudUIDoc\MonkeyDocument.cs shows how to implement UIDocument. public class MonkeyDocument : UIDocument { public MonkeyDocument (NSUrl url) : base (url) { DocumentString = "(default text)"; } // the 'model', just a chunk of text in this case; easily converts to NSData NSString dataModel; // model is wrapped in a nice .NET-friendly property public string DocumentString { get { return dataModel.ToString (); } set { dataModel = new NSString(value); } } // contents supplied by iCloud to display, update local model and display (via notification) public override bool LoadFromContents (NSObject contents, string typeName, out NSError outError) { outError = null; if (contents != null) { dataModel = NSString.FromData( (NSData)contents, NSStringEncoding.UTF8 ); } // LoadFromContents called when an update occurs NSNotificationCenter.DefaultCenter.PostNotificationName("monkeyDocumentModified", this); return true; } // return contents for iCloud to save (from the local model) public override NSObject ContentsForType (string typeName, out NSError outError) { outError = null; NSData docData = dataModel.Encode(NSStringEncoding.UTF8); return docData; } } The data model in this case is very simple - a single text field. Your data model can be as complex as required, such as an Xml document or binary data. The primary role of the UIDocument implementation is to translate between your model classes and an NSData representation that can be saved/loaded on disk. Finding and Opening iCloud Documents The sample app only deals with a single file - test.txt - so the code in AppDelegate.cs creates an NSPredicate and NSMetadataQuery to look specifically for that filename. The NSMetadataQuery runs asynchronously and sends a notification when it finishes. DidFinishGathering gets called by the notification observer, stops the query and calls LoadDocument, which uses the UIDocument.Open method with a completion handler to attempt to load the file and display it in a MonkeyDocumentViewController. string monkeyDocFilename = "test.txt"; void FindDocument () { query = new NSMetadataQuery(); query.SearchScopes = new NSObject [] {NSMetadataQuery.QueryUbiquitousDocumentsScope}; var pred = NSPredicate.FromFormat ("%K == %@" , new NSObject[] { NSMetadataQuery.ItemFSNameKey , new NSString(monkeyDocFilename)}); query.Predicate = pred; NSNotificationCenter.DefaultCenter.AddObserver (this , new Selector("queryDidFinishGathering:") , NSMetadataQuery.DidFinishGatheringNotification , query); query.StartQuery (); } [Export("queryDidFinishGathering:")] void DidFinishGathering (NSNotification notification) { var query = (NSMetadataQuery)notification.Object; query.DisableUpdates(); query.StopQuery(); NSNotificationCenter.DefaultCenter.RemoveObserver (this , NSMetadataQuery.DidFinishGatheringNotification , query); LoadDocument(query); } void LoadDocument (NSMetadataQuery query) { if (query.ResultCount == 1) { NSMetadataItem item = (NSMetadataItem)query.ResultAtIndex(0); var url = (NSUrl)item.ValueForAttribute(NSMetadataQuery.ItemURLKey); doc = new MonkeyDocument(url); doc.Open ( (success) => { if (success) { viewController.DisplayDocument (doc); } else Console.WriteLine ("failed to open iCloud document"); }); } // TODO: if no document, we need to create one } Displaying iCloud Documents Displaying a UIDocument shouldn't be any different to any other model class - properties are displayed in UI controls, possibly edited by the user and then written back to the model. In the example iCloudUIDoc\MonkeyDocumentViewController.cs displays the MonkeyDocument text in a UITextView. ViewDidLoad listens for the notification sent in the MonkeyDocument.LoadFromContents method. LoadFromContents is called when iCloud has new data for the file, so that notification indicates that the document has been updated. NSNotificationCenter.DefaultCenter.AddObserver (this , new Selector("dataReloaded:") , new NSString("monkeyDocumentModified"), null); The sample code notification handler calls a method to update the UI - in this case without any conflict detection or resolution. [Export("dataReloaded:")] void DataReloaded (NSNotification notification) { doc = (MonkeyDocument)notification.Object; // we just overwrite whatever was being typed, no conflict resolution for now docText.Text = doc.DocumentString; } Saving iCloud Documents To add a UIDocument to iCloud you can call UIDocument.Save directly (for new documents only) or move an existing file using NSFileManager.DefaultManager.SetUbiquitious. The example code creates a new document directly in the ubiquity container with this code (there are two completion handlers here, one for the Save operation and another for the Open): var docsFolder = Path.Combine(iCloudUrl.Path, "Documents"); // NOTE: Documents folder is user-accessible in Settings var docPath = Path.Combine (docsFolder, monkeyDocFilename); var ubiq = new NSUrl(docPath, false); var doc = new MonkeyDocument(ubiq); // gets the 'default content' doc.Save (doc.FileUrl, UIDocumentSaveOperation.ForCreating , (saveSuccess) => { // completion handler #1 if (saveSuccess) { doc.Open ((openSuccess) => { // completion handler #2 if (openSuccess) viewController.DisplayDocument (doc); // document is opened else Console.WriteLine ("couldn't open"); }); } else { Console.WriteLine ("couldn't save"); } }); Subsequent changes to the document are not "saved" directly, instead we tell the UIDocument that it has changed with UpdateChangeCount, and it will automatically schedule a save to disk operation: doc.UpdateChangeCount (UIDocumentChangeKind.Done); Managing iCloud Documents Users can manage iCloud documents in the Documents directory of the "ubiquity container" outside of your application via Settings; they can view the file list and swipe to delete. Application code should be able to handle the situation where documents are deleted by the user. Do not store internal application data in the Documents directory. Users will also receive different warnings when they attempt to remove an iCloud-enabled application from their device, to inform them of the status of iCloud documents related to that application. [ ](Images/iCloud_Delete2.png) iCloud Backup While backing up to iCloud isn't a feature that is directly accessed by developers, the way you design your application can affect the user experience. Apple provides iOS Data Storage Guidelines for developers to follow in their iOS applications. The most important consideration is whether your app stores large files that are not user-generated (for example, a magazine reader application that stores hundred-plus megabytes of content per issue). Apple prefers that you do not store this sort of data where it will be backed-up to iCloud and unnecessarily fill the user's iCloud quota. Applications that store large amounts of data like this should either store it in one of the user directories that is not backed-up (eg. Caches or tmp) or use NSFileManager.SetSkipBackupAttribute to apply a flag to those files so that iCloud ignores them during backup operations. Summary This article introduced the new iCloud feature included in iOS 5. It examined the steps required to configure your project to use iCloud and then provided examples of how to implement iCloud features. The key-value storage example demonstrated how iCloud can be used to store a small amount of data similar to the way NSUserPreferences are stored. The UIDocument example showed how more complex data can be stored and synchronized across multiple devices via iCloud. Finally it included a brief discussion on how the addition of iCloud Backup should influence your application design.
© Copyright 2024