Using iCloud Storage in an iOS 5 iPhone Application

From Techotopia
Jump to: navigation, search
PreviousTable of ContentsNext
Managing iPhone Files using the iOS 5 UIDocument ClassSynchronizing iPhone iOS 5 Key-Value Data using iCloud


Purchase the fully updated iOS 10 / Swift 3 / Xcode 8 edition of this book in eBook ($19.99) or Print ($45.99) format.
iOS 10 App Development Essentials Print and eBook (ePub/PDF/Kindle) edition contains over 100 chapters. Learn more...

Buy eBook Buy Print Preview Book


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 iPhone applications. Having outlined the steps necessary to enable iCloud access in the chapter entitled Preparing an iOS 5 iPhone App to use iCloud Storage, and provided an overview of the UIDocument class in Managing iPhone 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 iPhone 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.

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
{
    MyDocument *document;
    NSURL *documentURL;
    NSURL *ubiquityURL;
    UITextView *textView;
    NSMetadataQuery *metadataQuery;
}
@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"

@implementation iCloudStoreViewController
@synthesize textView, documentURL, document;
@synthesize ubiquityURL, metadataQuery;
.
.
@end

Declaring the UBUIQUITY_CONTAINER_URL Constant

When documents are saved to the cloud they will be placed in sub folders of a folder on iCloud using the following path:

/private/var/mobile/Library/Mobile Documents/<ubiquity container id>/

In the above path, <unique ubiquity id> is derived from the value of the ubiquity container identifier passed through to the URLForUbiquityContainerIdentifier method of the NSFileManager class. The ubiquity path is generally constructed using the same format as that used in the entitlements file, consisting of the developer ID, the reversed domain and the application’s App ID. For example:

ABCDE12345.com.yourcompany.iCloudStore

Following iCloud programming convention we declare this ID as a constant named UBUIQUITY_CONTAINER_URL and then reference it later when accessing iCloud storage. For the purposes of this example, edit the iCloudStoreViewController.m file and add the constant declaration (substituting your developer ID and domain name where appropriate):

#import "iCloudStoreViewController.h"
#define UBIQUITY_CONTAINER_URL @”ABCDE12345.com.yourcompany.iCloudStore"

@implementation iCloudStoreViewController
@synthesize textView, documentURL, document;
@synthesize ubiquityURL, metadataQuery;
.
.
@end

When used to construct a ubiquitous URL for a document that is to be stored as document.doc in the Documents sub folder, the resulting URL for the above ID will translate to:

/private/var/mobile/Library/Mobile Documents/
ABCDE12345~com~yourcompany~iCloudStore/
Documents/document.doc

Now that the preparatory steps are complete, it is time to start writing some code.

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 the UBIQUITY_CONTAINER_URL constant as an argument. Since it is recommended that documents be stored in the Documents sub-directory, this needs to be appended to the URL path:

ubiquityURL = [[filemgr 
     URLForUbiquityContainerIdentifier:UBIQUITY_CONTAINER_URL]     
     URLByAppendingPathComponent:@"Documents"];

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:UBIQUITY_CONTAINER_URL] 
       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 individual code fragments outlined above combine to implement the following metadataQueryDidFinishGathering: 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 iPhone device, but first some settings on the device need to be checked.

Enabling iCloud Document and Data Storage on an iPhone

Whether or not applications are permitted to use iCloud storage on an iPhone is controlled by the iCloud settings on that device. To review these settings, open the Settings application on the iPhone 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:


Configuring iCloud settings within the iOS 5 iPhone Settings app

Figure 28-1

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 iPhone 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. A list of documents stored using iCloud for the selected application will then appear including the current file size:


Using the iOS 5 iPhone settings application to view iCloud stored documents

Figure 28-2


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.

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.


Purchase the fully updated iOS 10 / Swift 3 / Xcode 8 edition of this book in eBook ($19.99) or Print ($45.99) format.
iOS 10 App Development Essentials Print and eBook (ePub/PDF/Kindle) edition contains over 100 chapters. Learn more...

Buy eBook Buy Print Preview Book



PreviousTable of ContentsNext
Managing iPhone Files using the iOS 5 UIDocument ClassSynchronizing iPhone iOS 5 Key-Value Data using iCloud