Using iCloud Storage in an iOS 5 iPad Application
Previous | Table of Contents | Next |
Managing Files using the iOS 5 UIDocument Class | Synchronizing iPad iOS 5 Key-Value Data using iCloud |
Learn SwiftUI and take your iOS Development to the Next Level |
The two preceding chapters of this book were intended to convey the knowledge necessary to begin implementing iCloud based document storage in iOS 5 based iPad applications. Having outlined the steps necessary to enable iCloud access in the chapter entitled Preparing an iOS 5 App to use iCloud Storage, and provided an overview of the UIDocument class in Managing Files using the iOS 5 UIDocument Class, the next step is to actually begin to store documents using the iCloud service.
Within this chapter the iCloudStore application created in the previous chapter will be re-purposed to store a document using iCloud storage instead of the local device based file system. The assumption is also made that the project has been enabled for iCloud storage following the steps outlined in Preparing an iOS 5 App to use iCloud Storage.
iCloud Usage Guidelines
Before implementing iCloud storage in an application there a few rules that must first be understood. Some of these are mandatory rules and some are simply recommendations made by Apple:
- Applications must be associated with a provisioning profile enabled for iCloud storage.
- The application projects must include a suitably configured entitlements file for iCloud storage.
- Applications should not make unnecessary use of iCloud storage. Once a user’s initial free iCloud storage space is consumed by stored data the user will either need to delete files for purchase more space.
- Applications should, ideally, provide the user with the option to select which documents are to be stored in the cloud and which are to stored locally.
- When opening a previously created iCloud based document the application should never use an absolute path to the document. The application should instead search for the document by name in the application’s iCloud storage area and then access it using the result of the search.
- Documents stored using iCloud should be placed in the application’s Documents directory. This gives the user the ability to delete individual documents from the storage. Documents saved outside the Document folder can only be deleted in bulk.
- When creating a document for the first time it is recommended that the document be created locally first and then moved into iCloud storage.
Preparing the iCloudStore Application for iCloud Access
Much of the work performed in creating the local storage version of the iCloudStore application in the previous chapter will be reused in this example. The user interface, for example, remains unchanged and the implementation of the UIDocument subclass will not need to be modified. In fact, the only methods that need to be rewritten are the saveDocument and viewDidLoad methods of the view controller.
Load the iCloudStore project into Xcode and select the iCloudStoreViewController.m file. Locate the saveDocument method and remove the current code from within the method so that it reads as follows:
- (void)saveDocument { }
Next, locate the viewDidLoad method and modify it accordingly to match the following fragment:
- (void)viewDidLoad { [super viewDidLoad]; }
Configuring the View Controller
Before writing any code there are a number of variables that need to be defined within the view controller’s iCloudStoreViewController.h interface file in addition to those implemented in the previous chapter.
In addition to the URL of the local version of the document, it will also now be necessary to create a URL to the document location in the iCloud storage. When a document is stored on iCloud it is said to be ubiquitous since the document is accessible to the application regardless of the device on which it is running. The object used to store this URL will, therefore, be named ubiquityURL.
As previously stated, when opening a stored document, an application should search for the document rather than directly access it using a stored path. An iCloud document search is performed using an NSMetaDataQuery object which needs to be declared in the interface file for the view controller, in this instance using the name metaDataQuery. Note that declaring the object locally to the method in which it is used will result in the object being released by the automatic array counting service (ARC) before it has completed the search.
To implement these requirements, select the iCloudStoreViewController.h file in the Xcode project navigator panel and modify the file as follows:
#import <UIKit/UIKit.h> #import "MyDocument.h" @interface iCloudStoreViewController : UIViewController @property (strong, nonatomic) IBOutlet UITextView *textView; @property (strong, nonatomic) NSURL *documentURL; @property (strong, nonatomic) MyDocument *document; @property (strong, nonatomic) NSURL *ubiquityURL; @property (strong, nonatomic) NSMetadataQuery *metadataQuery; -(IBAction)saveDocument; @end
Next, edit the iCloudStoreViewController.m file and add the corresponding @synthesize directive for the new class members:
#import "iCloudStoreViewController.h" @interface iCloudStoreViewController () @end @implementation iCloudStoreViewController @synthesize textView, documentURL, document; @synthesize ubiquityURL, metadataQuery; . . @end
Implementing the viewDidLoad Method
The purpose of the code in the view controller viewDidLoad method is to construct both the URL to the local version of the file (assigned to documentURL) and the URL for the ubiquitous version stored using iCloud (assigned to ubiquityURL). For documentURL it is first necessary to identify the location of the application’s Documents directory, create the full path to the document.doc file and then initialize the NSURL object:
NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docsDir = [dirPaths objectAtIndex:0]; NSString *dataFile = [docsDir stringByAppendingPathComponent: @"document.doc"]; self.documentURL = [NSURL fileURLWithPath:dataFile];
The ubiquitous URL is constructed by calling the URLForUbiquityContainerIdentifier: method of the NSFileManager passing through nil as an argument in order to default to the first container listed in the entitlements file. Since it is recommended that documents be stored in the Documents sub-directory, this needs to be appended to the URL path:
</pre> ubiquityURL = [[filemgr
URLForUbiquityContainerIdentifier:nil] URLByAppendingPathComponent:@"Documents"];
</pre>
By default, the iCloud storage area for the application will not already contain a Documents sub-directory so the next step is to check to see if the sub-directory already exists and, in the event that is does not, create it:
if ([filemgr fileExistsAtPath:[ubiquityURL path]] == NO) [filemgr createDirectoryAtURL:ubiquityURL withIntermediateDirectories:YES attributes:nil error:nil];
Having created the Documents directory if necessary, the next step is to append the document name (document.doc) to the end of the ubuiquityURL path:
ubiquityURL = [ubiquityURL URLByAppendingPathComponent:@"document.doc"];
The final task for the viewDidLoad method is to initiate a search in the application’s iCloud storage area to find out if the document.doc file already exists and to act accordingly subject to the result of the search. The search is performed by calling the methods on an instance of the NSMetaDataQuery object. This involves creating the object, setting a predicate to indicate the files to search for and defining a ubiquitous search scope (in other words instructing the object to search iCloud storage). Once initiated, the search is performed on a separate thread and issues a notification when completed. For this reason, it is also necessary to configure an observer to be notified when the search is finished. The code to perform these tasks reads as follows:
metadataQuery = [[NSMetadataQuery alloc] init]; [metadataQuery setPredicate:[NSPredicate predicateWithFormat:@"%K like 'document.doc'", NSMetadataItemFSNameKey]]; [metadataQuery setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope,nil]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryDidFinishGathering:) name: NSMetadataQueryDidFinishGatheringNotification object:metadataQuery]; [metadataQuery startQuery];
Once the [metadataQuery startQuery] method is called the search will run and trigger the metadataQueryDidFinishGathering: method once the search is complete. The next step, therefore, is to implement the metadataQueryDidFinishGathering: method. Before doing so, however, note that the viewDidLoad method is now complete and the full implementation should read as follows:
- (void)viewDidLoad { [super viewDidLoad]; NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docsDir = [dirPaths objectAtIndex:0]; NSString *dataFile = [docsDir stringByAppendingPathComponent: @"document.doc"]; self.documentURL = [NSURL fileURLWithPath:dataFile]; NSFileManager *filemgr = [NSFileManager defaultManager]; [filemgr removeItemAtURL:documentURL error:nil]; ubiquityURL = [[filemgr URLForUbiquityContainerIdentifier:nil] URLByAppendingPathComponent:@"Documents"]; if ([filemgr fileExistsAtPath:[ubiquityURL path]] == NO) [filemgr createDirectoryAtURL:ubiquityURL withIntermediateDirectories:YES attributes:nil error:nil]; ubiquityURL = [ubiquityURL URLByAppendingPathComponent:@"document.doc"]; // Search for document in iCloud storage metadataQuery = [[NSMetadataQuery alloc] init]; [metadataQuery setPredicate:[NSPredicate predicateWithFormat:@"%K like 'document.doc'", NSMetadataItemFSNameKey]]; [metadataQuery setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope,nil]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryDidFinishGathering:) name: NSMetadataQueryDidFinishGatheringNotification object:metadataQuery]; [metadataQuery startQuery]; }
Implementing the metadataQueryDidFinishGathering: Method
When the meta data query was triggered in the viewDidLoad method to search for documents in the application’s iCloud storage area, an observer was configured to call a method named metadataQueryDidFinishGathering when the initial search completed. The next logical step is to implement this method. The first task of the method is to identify the query object that caused this method to be called. This object must then be used to disable any further query updates (at this stage the document either exists or doesn’t exist so there is nothing to be gained by receiving additional updates) and stop the search. It is also necessary to remove the observer that triggered the method call. Combined, these requirements result in the following code:
NSMetadataQuery *query = [notification object]; [query disableUpdates]; [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query]; [query stopQuery];
Next, the query method of the query object needs to be called to extract an array of documents located during the search:
NSArray *results = [[NSArray alloc] initWithArray:[query results]];
A more complex application would, in all likelihood, need to implement a for loop to iterate through more than one document in the array. Given that the iCloudStore application searched for only one specific file name we can simply check the array element count and assume that if the count is 1 then the document already exists. In this case, the ubiquitous URL of the document from the query object needs to be assigned to our ubiquityURL member property and used to create an instance of our MyDocument class called document. The openWithCompletionHandler: method of the document object is then called to open the document in the cloud and read the contents. This will trigger a call to the loadFromContents method of the document object which, in turn, will assign the contents of the document to the userText property. Assuming the document read is successful the value of userText needs to be assigned to the text property of the text view object to make it visible to the user. Bringing this together results in the following code fragment:
if ([results count] == 1) { // File exists in cloud so get URL ubiquityURL = [[results objectAtIndex:0] valueForAttribute:NSMetadataItemURLKey]; self.document = [[MyDocument alloc] initWithFileURL:ubiquityURL]; [document openWithCompletionHandler: ^(BOOL success) { if (success){ NSLog(@"Opened iCloud doc"); textView.text = document.userText; } else { NSLog(@"Failed to open iCloud doc"); } }]; } else { }
In the event that the document does not yet exist in iCloud storage the code needs to create the document using the saveToURL method of the document object passing through the value of ubiquityURL as the destination path on iCloud:
. . } else { self.document = [[MyDocument alloc] initWithFileURL:ubiquityURL]; [document saveToURL:ubiquityURL forSaveOperation: UIDocumentSaveForCreating completionHandler:^(BOOL success) { if (success){ NSLog(@"Saved to cloud"); } else { NSLog(@"Failed to save to cloud"); } }]; } }
The above individual code fragments combine to implement the following metadataQueryDidFinish-Gathering: method:
- (void)metadataQueryDidFinishGathering: (NSNotification *)notification { NSMetadataQuery *query = [notification object]; [query disableUpdates]; [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query]; [query stopQuery]; NSArray *results = [[NSArray alloc] initWithArray:[query results]]; if ([results count] == 1) { // File exists in cloud so get URL ubiquityURL = [[results objectAtIndex:0] valueForAttribute:NSMetadataItemURLKey]; self.document = [[MyDocument alloc] initWithFileURL:ubiquityURL]; [document openWithCompletionHandler: ^(BOOL success) { if (success){ NSLog(@"Opened iCloud doc"); textView.text = document.userText; } else { NSLog(@"Failed to open iCloud doc"); } }]; } else { // File does not exist in cloud. self.document = [[MyDocument alloc] initWithFileURL:ubiquityURL]; [document saveToURL:ubiquityURL forSaveOperation: UIDocumentSaveForCreating completionHandler:^(BOOL success) { if (success){ NSLog(@"Saved to cloud"); } else { NSLog(@"Failed to save to cloud"); } }]; }
Implementing the saveDocument Method
The final task before building and running the application is to implement the saveDocument method. This method simply needs to update the userText property of the document object with the text entered into the text view and then call the saveToURL method of the document object, passing through the ubiquityURL as the destination URL using the UIDocumentSaveForOverwriting option:
- (void)saveDocument { self.document.userText = textView.text; [self.document saveToURL:ubiquityURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) { if (success){ NSLog(@"Saved to cloud for overwriting"); } else { NSLog(@"Not saved to cloud for overwriting"); } }]; }
All that remains now is to build and run the iCloudStore application on an iPad device, but first some settings on the device need to be checked.
Enabling iCloud Document and Data Storage on an iPad
Whether or not applications are permitted to use iCloud storage on an iPad is controlled by the iCloud settings on that device. To review these settings, open the Settings application on the iPad and select the iCloud category. Scroll down the list of various iCloud related options and verify that the Documents & Data option is set to On.
Running the iCloud Application
Test the iCloudStore app by connecting a suitably provisioned device to the development Mac OS X system, selecting it from the Xcode target menu and clicking on the Run button. Enter text into the text view and touch the Save button. In the Xcode toolbar click on Stop to exit the application followed by Run to re-launch the application. On the second launch the previously entered text will be read from the document in the cloud and displayed in the text view object.
Reviewing and Deleting iCloud Based Documents
The files currently stored in a user’s iCloud account may be reviewed or deleted from the iPad Settings app. To review the currently stored documents select the iCloud option from the main screen of the Settings app. On the iCloud screen, scroll to the bottom and select the Storage & Backup option. On the resulting screen, select Manage Storage followed by the name of the application for which stored documents are to be listed (may be listed as unknown). A list of documents stored using iCloud for the selected application will then appear including the current file size.
To delete the document, select the Edit button located in the toolbar. All listed documents may be deleted using the Delete All button, or deleted individually.
Figure 35-1
Making a Local File Ubiquitous
In addition to writing a file directly to iCloud storage as illustrated in this example application, it is also possible to transfer a pre-existing local file to iCloud storage, thereby making it ubiquitous. This can be achieved using the setUbiquitous method of the NSFileManager class. Assuming that documentURL references the path to the local copy of the file, and ubiquityURL the iCloud destination, a local file can be made ubiquitous using the following code:
NSFileManager *filemgr = [NSFileManager defaultManager]; NSError *error = nil; if ([filemgr setUbiquitous:YES itemAtURL:documentURL destinationURL:ubiquityURL error:&error] == YES) { NSLog(@"setUbiquitous OK"); } else NSLog(@"setUbiquitous Failed error = %@", error);
Summary
The objective of this chapter was to work through the process of developing an application that stores a document using the iCloud service. Both techniques of directly creating a file in the iCloud storage, and making an existing locally created file ubiquitous were covered. In addition, some important guidelines that should be observed when using iCloud were outlined.
Learn SwiftUI and take your iOS Development to the Next Level |
Previous | Table of Contents | Next |
Managing Files using the iOS 5 UIDocument Class | Synchronizing iPad iOS 5 Key-Value Data using iCloud |