Using iCloud Storage in an iOS 10 Application

From Techotopia
Revision as of 04:27, 10 November 2016 by Neil (Talk | contribs) (iCloud Usage Guidelines)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

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


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

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

Before starting on this project it is important to note that membership to the Apple Developer Program will be required as outlined in Joining the Apple Developer Program.


Contents


iCloud Usage Guidelines

Before implementing iCloud storage in an application there are 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 or 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 be 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 Documents 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 ViewController.swift file. Locate the saveDocument method and remove the current code from within the method so that it reads as follows:

@IBAction func saveDocument(_ sender: AnyObject) {
}

Next, locate the viewDidLoad method and modify it accordingly to match the following fragment:

override func viewDidLoad() {
    super.viewDidLoad()
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


Configuring the View Controller

Before writing any code there are a number of variables that need to be defined within the view controller’s ViewController.swift file in addition to those implemented in the previous chapter. 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 view controller class, 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 reference counting system (ARC) before it has completed the search.

To implement these requirements, select the ViewController.swift file in the Xcode project navigator panel and modify the file as follows:

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!

    var document: MyDocument?
    var documentURL: URL?
    var ubiquityURL: URL?
    var metaDataQuery: NSMetadataQuery?
.
.
.
}

Implementing the viewDidLoad Method

The purpose of the code in the view controller viewDidLoad method is to identify the URL for the ubiquitous version of the file to be stored using iCloud (assigned to ubiquityURL). The ubiquitous URL is constructed by calling the url(forUbiquityContainerIdentifier:) method of the FileManager passing through nil as an argument to default to the first container listed in the entitlements file.

ubiquityURL = filemgr.url(forUbiquityContainerIdentifier: nil)

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

The app will only be able to obtain the ubiquityURL if the user has configured a valid Apple ID within the iCloud page of the iOS Settings app. Some defensive code needs to be added, therefore, to notify the user and return from the viewDidLoad method if a valid ubiquityURL could not be obtained. For the purposes of testing in this example we will simply output a message to the console before returning:

guard ubiquityURL != nil else {
    print("Unable to access iCloud Account")
    print("Open the Settings app and enter your Apple ID into iCloud settings")
    return
} 

Since it is recommended that documents be stored in the Documents sub-directory, this needs to be appended to the URL path along with the file name:

ubiquityURL = 
	ubiquityURL?.appendingPathComponent("Documents/savefile.txt")

The final task for the viewDidLoad method is to initiate a search in the application’s iCloud storage area to find out if the savefile.txt 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 within the Documents directory of the app’s iCloud storage area). 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()

metaDataQuery?.predicate =
    NSPredicate(format: "%K like 'savefile.txt'",
        NSMetadataItemFSNameKey)
metaDataQuery?.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]

NotificationCenter.default.addObserver(self,
    selector: #selector(
           ViewController.metadataQueryDidFinishGathering),
    name: NSNotification.Name.NSMetadataQueryDidFinishGathering,
    object: metaDataQuery!)

metaDataQuery!.start()

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Once the start method is called, the search will run and call 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:

override func viewDidLoad() {
    super.viewDidLoad()

    let filemgr = FileManager.default

    ubiquityURL = filemgr.url(forUbiquityContainerIdentifier: nil)

    guard ubiquityURL != nil else {
        print("Unable to access iCloud Account")
        print("Open the Settings app and enter your Apple ID into iCloud settings")
        return
    }

    ubiquityURL = ubiquityURL?.appendingPathComponent(
					"Documents/savefile.txt")

    metaDataQuery = NSMetadataQuery()

    metaDataQuery?.predicate =
        NSPredicate(format: "%K like 'savefile.txt'",
            NSMetadataItemFSNameKey)
    metaDataQuery?.searchScopes = 
		[NSMetadataQueryUbiquitousDocumentsScope]

    NotificationCenter.default.addObserver(self,
        selector: #selector(
               ViewController.metadataQueryDidFinishGathering),
        name: NSNotification.Name.NSMetadataQueryDidFinishGathering,
        object: metaDataQuery!)

    metaDataQuery!.start()
}

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Implementing the metadataQueryDidFinishGathering Method

When the meta data query was triggered in the viewDidLoad method to search for documents in the Documents directory of 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:

let query: NSMetadataQuery = notification.object as! NSMetadataQuery

query.disableUpdates()

NotificationCenter.default.removeObserver(self,
        name: NSNotification.Name.NSMetadataQueryDidFinishGathering,
        object: query)

query.stop()

The next step is to make sure at least one match was found and to extract the URL of the first document located during the search:

if query.resultCount == 1 {
	let resultURL = query.value(ofAttribute: NSMetadataItemURLKey, 
			forResultAt: 0) as! URL

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 open(completionHandler:) 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 load(fromContents:) 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:

    document = MyDocument(fileURL: resultURL as URL)

    document?.open(completionHandler: {(success: Bool) -> Void in
        if success {
            print("iCloud file open OK")
            self.textView.text = self.document?.userText
            self.ubiquityURL = resultURL as URL
        } else {
            print("iCloud file open failed")
        }
    })
} else {
}

In the event that the document does not yet exist in iCloud storage the code needs to create the document using the save(to:) method of the document object passing through the value of ubiquityURL as the destination path on iCloud:

.
.
} else {
    document = MyDocument(fileURL: ubiquityURL!)

    document?.save(to: ubiquityURL!,
        for: .forCreating,
        completionHandler: {(success: Bool) -> Void in
            if success {
                print("iCloud create OK")
            } else {
                print("iCloud create failed")
        }
    })
} 

The individual code fragments outlined above combine to implement the following metadataQueryDidFinishGathering method which should be added to the ViewController.swift file:

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

func metadataQueryDidFinishGathering(notification: NSNotification) -> Void
{
    let query: NSMetadataQuery = notification.object as! NSMetadataQuery

    query.disableUpdates()

    NotificationCenter.default.removeObserver(self,
        name: NSNotification.Name.NSMetadataQueryDidFinishGathering,
      object: query)

    query.stop()

    let resultURL = query.value(ofAttribute: NSMetadataItemURLKey, 
			forResultAt: 0) as! URL

    if query.resultCount == 1 {
        let resultURL = query.value(ofAttribute: NSMetadataItemURLKey, 
				forResultAt: 0) as! URL

        document = MyDocument(fileURL: resultURL as URL)

        document?.open(completionHandler: {(success: Bool) -> Void in
            if success {
                print("iCloud file open OK")
                self.textView.text = self.document?.userText
                self.ubiquityURL = resultURL as URL
            } else {
                print("iCloud file open failed")
            }
        })
    } else {
        document = MyDocument(fileURL: ubiquityURL!)

        document?.save(to: ubiquityURL!,
                       for: .forCreating,
                       completionHandler: {(success: Bool) -> Void in
                        if success {
                            print("iCloud create OK")
                        } else {
                            print("iCloud create failed")
                        }
        })
    }
} 

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:

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

@IBAction func saveDocument(_ sender: AnyObject) {

    document!.userText = textView.text

        document?.save(to: ubiquityURL!,
                       for: .forOverwriting,
              completionHandler: {(success: Bool) -> Void in
        if success {
            print("Save overwrite OK")
        } else {
            print("Save overwrite failed")
        }
    })
}

All that remains now is to build and run the iCloudStore application on an iOS device, but first some settings need to be checked.

Enabling iCloud Document and Data Storage

When testing iCloud on an iOS Simulator session, it is important to make sure that the simulator is configured with a valid Apple ID within the account settings app. To configure this, launch the simulator, load the Settings app and click on the iCloud option. If no account information is configured on this page, enter a valid Apple ID and corresponding password before proceeding with the testing.

Whether or not applications are permitted to use iCloud storage on an iOS device or Simulator is controlled by the iCloud settings. To review these settings, open the Settings application on the device or simulator and select the iCloud category. Scroll down the list of various iCloud related options and verify that the iCloud Drive option is set to On:

Enabling iCloud drive within iOS


Figure 41-1


Running the iCloud Application

Once you have logged in to an iCloud account on the device or simulator, test the iCloudStore app clicking on the run button. Once running, edit the text in the text view and touch the Save button. In the Xcode toolbar click on the stop button to exit the application followed by the run button 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.

Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book

Reviewing and Deleting iCloud Based Documents

The files currently stored in a user’s iCloud account may be reviewed or deleted from the iOS Settings app running on a physical device. To review the currently stored documents select the iCloud option from the main screen of the Settings app. On the iCloud screen select the Storage option and, 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:


Reviewing files stored on iCloud from the iOS Settings app

Figure 41-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 FileManager 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:

do {
    try filemgr.setUbiquitous(true, itemAt: documentURL!,
                destinationURL: ubiquityURL!)
} catch let error {
    print("setUbiquitous failed: \(error.localizedDescription)")
}

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 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
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book



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