iCloud and Data Storage - Swift Development with Cocoa (2015)

Swift Development with Cocoa (2015)

Chapter 10. iCloud and Data Storage

Unless your application is a trivial one, it will need to work with data at some point. This data could be as simple as a list of high scores that the user has achieved, or as complex as a multimedia document like a presentation.

This information needs to be accessible to other parts of your application, such as the controller objects, so that work can be done on it. The information needs to be stored somewhere—either in memory, on disk, or on the network.

OS X and iOS provide tools for storing information on disk and on the network. One of the more recent additions to the APIs available to developers is iCloud, a network-based storage system that is designed to allow users to keep the same information on all their devices, without having to do any work to enable this.

In this chapter, you will learn how to work with the filesystem to store your information on disk, and how to store simple data in the built-in user preferences database. You’ll also learn how to work with iCloud storage to store data and files in the cloud. Finally, you’ll learn how the sandbox works on OS X, and how to use security-scoped bookmarks to allow your application to access data outside its sandbox across multiple launches.

NOTE

While iCloud provides the means for storing files and folders in the cloud, you also need to know how to present documents to the user. This chapter only covers the mechanics of storing the data; to learn more about how to write a document-based application on OS X and iOS, head to Chapter 13.

Preferences

Most applications need to store some information about the user’s preferences. For example, if you open the Safari web browser and go to its preferences (by pressing ⌘-, [comma] or choosing Safari→Preferences), you’ll see a rather large collection of settings that the user can modify. Because these settings need to remain set when the application exits, they need to be stored somewhere.

The NSUserDefaults class allows you to store settings information in a key-value based way. You don’t need to handle the process of loading and reading in a settings file, and preferences are automatically saved.

To access preferences stored in NSUserDefaults, you need an instance of the NSUserDefaults class. To get one, you must ask the NSUserDefaults class for the standardUserDefaults:

let defaults = NSUserDefaults.standardUserDefaults()

NOTE

It’s also possible to create a new NSUserDefaults object instead of using the standard user defaults. You only need to do this if you want more control over exactly whose preferences are being accessed. For example, if you are creating an application that manages multiple users on a Mac and accesses their preferences, you can create an NSUserDefaults object for each user’s preferences.

Registering Default Preferences

When your application obtains a preferences object for the first time (i.e., on the first launch of your application), that preferences object is empty. In order to create default values, you need to provide a dictionary containing the defaults to the defaults object.

NOTE

The word default gets tossed around quite a lot when talking about the defaults system. To clarify:

§ A defaults object is an instance of the class NSUserDefaults.

§ A default is a setting inside the defaults object.

§ A default value is a setting used by the defaults object when no other value has been set. (This is the most common meaning of the word when talking about non-Cocoa environments.)

To register default values in the defaults object, you first need to create a dictionary. The keys of this dictionary are the same as the names of the preferences, and the values associated with these keys are the default values of these settings. Once you have the dictionary, you provide it to the defaults object with the registerDefaults method:

// Create the default values dictionary

let myDefaults = [

"greeting": "hello",

"numberOfItems": 1

]

// Provide this dictionary to the defaults object

defaults.registerDefaults(myDefaults)

Once this is done, you can ask the defaults object for values.

NOTE

The defaults that you register with the registerDefaults method are not saved on disk, which means that you need to call this every time your application starts up. Defaults that you set in your application (see Setting Preferences) are saved, however.

Accessing Preferences

Once you have a reference to one, an NSUserDefaults object can be treated much like a dictionary, with a few restrictions. You can retrieve a value from the defaults object by using the objectForKey method:

// Retrieve a string with the key "greeting" from the defaults object

let greeting = defaults.objectForKey("greeting") as? String

Only a few kinds of objects can be stored in a defaults object. The only objects that can be stored in a defaults object are property list objects, which are:

§ Strings

§ Numbers

§ NSData

§ NSDate

§ Arrays and dictionaries (as long as they only contain items in this list)

If you need to store any other kind of object in a defaults object, you should first convert it to an NSData by archiving it (see Serialization and Deserialization in Chapter 2).

NOTE

Additional methods exist for retrieving values from an NSUserDefaults object. For more information, see the Preferences and Settings Programming Guide, available in the Xcode documentation.

Setting Preferences

In addition to retrieving values from a defaults object, you can also set values. When you set a value in an NSUserDefaults object, that value is kept around forever (until the application is removed from the system).

To set an object in an NSUserDefaults object, you use the setObject(_,forKey:) method:

let newGreeting = "hi there"

defaults.setObject(newGreeting, forKey: "greeting")

Working with the Filesystem

Most applications work with data stored on disk, and data is most commonly organized into files and folders. An increasing amount of data is also stored in cloud services, like Dropbox and Google Drive.

All Macs and iOS devices have access to iCloud, Apple’s data synchronization and storage service. The idea behind iCloud is that users can have the same information on all the devices and computers they own, and don’t have to manually sync or update anything—all synchronization and updating is done by the computer.

Because the user’s documents can exist as multiple copies spread over different cloud storage services, it’s now more and more the case that working with the user’s data means working with one of potentially many copies of that data. This means that the copy of the data that exists on the current machine may be out of date or may conflict with another version of the data. iCloud works to reduce the amount of effort required to solve these issues, but they’re factors that your code needs to be aware of.

Cocoa provides a number of tools for working with the filesystem and with files stored in iCloud, which is discussed later in this chapter in iCloud.

NOTE

This chapter deals with files in the filesystem, which is only half the story of making a full-fledged, document-based application. To learn how to create an application that deals with documents, turn to Chapter 13.

Files may be stored in one of two places: either inside the application’s bundle or elsewhere on the disk.

Files that are stored in the application’s bundle are kept inside the .app folder and distributed with the app. If the application is moved on disk (e.g., if you were to drag it to another location on your Mac), the resources move with the app.

When you add a file to a project in Xcode, it is added to the current target (though you can choose for this not to happen). Then, when the application is built, the file is copied into the relevant part of the application bundle, depending on the OS—on OS X, the file is copied into the bundle’sResources folder, while on iOS, it is copied into the root folder of the bundle.

Files copied into the bundle are mostly resources used by the application at runtime—sounds, images, and other things needed for the application to run. The user’s documents aren’t stored in this location.

NOTE

If a file is stored in the application bundle, it’s part of the code-signing process—changing, removing, or adding a file to the bundle after it’s been code-signed will cause the OS to refuse to launch the app. This means that files stored in the application bundle are read-only.

Retrieving a file from the application’s bundle is quite straightforward, and is covered in more detail in Using NSBundle to Find Resources in Applications. This chapter covers how to work with files that are stored elsewhere.

NOTE

Some files are processed when they’re copied into the application bundle. For example, .xib files are compiled from their XML source into a more quickly readable binary format, and on iOS, PNG images are processed so that the device’s limited GPU can load them more easily (though this renders them unopenable with apps like Preview). Don’t assume that files are simply copied into the bundle!

Using NSFileManager

Applications can access files almost anywhere on the system. The “almost anywhere” depends on which OS your application is running on, and whether the application exists within a sandbox.

NOTE

Sandboxes, which are discussed later in this chapter in Working with the Sandbox, restrict what your application is allowed to access. So even if your application is compromised by malicious code, for example, it cannot access files that the user does not want it to.

By default, the sandbox is limited to the application’s private working space, and cannot access any user files. To gain access to these files, you make requests to the system, which handle the work of presenting the file-selection box to the user and open holes in the sandbox for working with the files the user wants to let your application access (and only those files).

Your interface to the filesystem is the NSFileManager object, which allows you to list the contents of folders; create, rename, and delete files; modify attributes of files and folders; and generally perform all the filesystem tasks that the Finder does.

To access the NSFileManager class, you use the shared manager object:

let fileManager = NSFileManager.defaultManager()

NOTE

NSFileManager allows you to set a delegate on it, which receives messages when the file manager completes operations like copying or moving files. If you are using this feature, you should create your own instance of NSFileManager instead of using the shared object:

let fileManager = NSFileManager()

// we can now set a delegate on this new file manager to be

// notified when operations are complete

fileManager.delegate = self

You can use NSFileManager to get the contents of a folder, using the following method: contentsOfDirectoryAtURL(_,includingPropertiesForKeys:options:error:). This method can be used to simply return NSURLs for the contents of a folder, but also to fetch additional information about a file:

let folderURL = NSURL.fileURLWithPath("/Applications/")

var error : NSError? = nil

let folderContents = fileManager.contentsOfDirectoryAtURL(folderURL!,

includingPropertiesForKeys:nil, options:NSDirectoryEnumerationOptions(),

error:&error)

After this call, the array folderContents contains NSURLs that point to each item in the folder. If there was an error, the method returns nil, and the error variable contains an NSError object that describes exactly what went wrong.

You can also ask the individual NSURL objects for information about the file that they point to. You can do this via the resourceValuesForKeys(_,error:) method, which returns a dictionary that contains the attributes for the item pointed to by the URL:

// anURL is an NSURL object

// Pass in an array containing the attributes you want to know about

let attributes = [NSURLFileSizeKey, NSURLContentModificationDateKey]

// In this case, we don't care about any potential errors, so we

// pass in 'nil' for the error parameter.

let attributesDictionary = anURL.resourceValuesForKeys(attributes, error: nil)

// We can now get the file size out of the dictionary:

let fileSizeInBytes = attributesDictionary?[NSURLFileSizeKey] as NSNumber

// And the date it was last modified:

let lastModifiedDate =

attributesDictionary?[NSURLContentModificationDateKey] as NSDate

NOTE

Checking each attribute takes time, so if you need to get attributes for a large number of files, it makes more sense to instruct the NSFileManager to pre-fetch the attributes when listing the directory’s contents:

let attributes =

[NSURLFileSizeKey, NSURLContentModificationDateKey]

fileManager.contentsOfDirectoryAtURL(folderURL,

includingPropertiesForKeys: attributes,

options: NSDirectoryEnumerationOptions(), error: nil)

Getting a temporary directory

It’s often very convenient to have a temporary directory that your application can put files in. For example, if you’re downloading some files, and want to save them somewhere temporarily before moving them to their final location, a temporary directory is just what you need.

To get the location of a temporary directory that your application can use, you use the NSTemporaryDirectory function:

let temporaryDirectoryPath = NSTemporaryDirectory()

This function returns a string, which contains the path of a directory you can store files in. If you want to use it as an NSURL, you’ll need to use the fileURLWithPath method to convert it.

WARNING

Files in a temporary directory are subject to deletion without warning. If the operating system decides it needs more disk space, it will begin deleting the contents of temporary directories. Don’t put anything important in the temporary directory!

Creating directories

Using NSFileManager, you can create and remove items on the filesystem. To create a new directory, for example, use:

let newDirectoryURL = NSURL.fileURLWithPath(temporaryDirectoryPath +

"/MyNewDirectory")

var error : NSError? = nil

var didCreate = fileManager.createDirectoryAtURL(newDirectoryURL!,

withIntermediateDirectories: false, attributes: nil, error: &error)

if (didCreate) {

// The directory was successfully created

} else {

// The directory wasn't created (maybe one already exists at the path?)

// More information is stored in the 'error' variable

}

Note that you can pass in an NSDictionary containing the desired attributes for the new directory.

NOTE

If you set a YES value for the withIntermediateDirectories parameter, the system will create any additional folders that are necessary to create the folder. For example, if you have a folder named Foo, and want to have a folder named Foo/Bar/Bas, you would create an NSURL that points to the second folder and ask the NSFileManager to create it. The system would create the Bar folder, and then create the Bas folder inside that.

Creating files

Creating files works the same way. You provide a path in an NSString, the NSData that the file should contain, and an optional dictionary of attributes that the file should have:

// Note that the first parameter is the path (as a string), NOT an NSURL!

fileManager.createFileAtPath(newFilePath!,

contents: newFileData,

attributes: nil)

Removing files

Given a URL, NSFileManager is also able to delete files and directories. You can only delete items that your app has permission to delete, which limits your ability to write a program that accidentally erases the entire system.

To remove an item, you do this:

fileManager.removeItemAtURL(newFileURL!, error: nil)

WARNING

There’s no undo for removing files or folders using NSFileManager. Items aren’t moved to the Trash—they’re immediately deleted.

Moving and copying files

To move a file, you need to provide both an original URL and a destination URL. You can also copy a file, which duplicates it and places the duplicate at the destination URL.

To move an item, you do this:

fileManager.moveItemAtURL(sourceURL!, toURL: destinationURL, error: nil)

To copy an item, you do this:

fileManager.copyItemAtURL(sourceURL!, toURL: destinationURL, error: nil)

Just like all the other file manipulation methods, these methods return true on success, and false if there was a problem.

File Storage Locations

There are a number of existing locations where the user can keep files. These include the Documents directory, the Desktop, and common directories that the user may not ever see, such as the Caches directory, which is used to store temporary files that the application would find useful to have around but could regenerate if needed (like downloaded images).

Your code can quickly determine the location of these common directories by asking the NSFileManager class. To do this, you use the URLsForDirectory(_,inDomains:) method in NSFileManager, which returns an array of NSURL objects that point to a directory that matches the kind of location you asked for. For example, to get an NSURL that points to the user’s Documents directory, you do this:

let URLs = fileManager.URLsForDirectory(NSSearchPathDirectory.DocumentDirectory,

inDomains: NSSearchPathDomainMask.UserDomainMask) as [NSURL]

let documentURL = URLs[0]

You can then use this URL to create additional URLs. For example, to generate a URL that points to a file called Example.txt in your Documents directory, you can use URLByAppendingPathComponent:

let fileURL = documentURL.URLByAppendingPathComponent("Example.txt")

Working with the Sandbox

An application that runs in a sandbox may only access files that exist inside that sandbox, and is allowed to read and write without restriction inside its designated sandbox container. In addition, if the user has granted access to a specific file or folder, the sandbox will allow your application to read and/or write to that location as well.

NOTE

If you want to put your application in the Mac App Store, it must be sandboxed. Apple will reject your application if it isn’t. All iOS apps are automatically sandboxed by the system.

Enabling Sandboxing

To turn on sandboxing, follow these steps.

1. Select your project at the top of the navigation pane.

2. In the Capabilities tab, scroll to App Sandbox.

3. Turn on App Sandboxing.

Your application will then launch in sandboxed mode, which means that it won’t be able to access any resources that the system does not permit it to.

NOTE

To use the sandbox, you need to have a Mac developer identity. To learn more about getting one, see Chapter 1.

In the sandbox setup screen, you can specify what the application should have access to. For example, if you need to be able to read and write files in the user’s Music folder, you can change the Music Folder Access setting from None (the default) to Read Access or Read/Write Access.

If you want to let the user choose which files and folders should be accessible, change User Selected File Access to something other than None.

Open and Save Panels

One way that you can let the user indicate that your app is allowed to access a file is to use an NSOpenPanel or NSSavePanel. These are the standard open and save windows that you’ve seen before; however, when your application is sandboxed, the panel being displayed is actually not being shown by your application, but rather by a built-in system component called Powerbox. When you display an open or save panel, Powerbox handles the process of selecting the files; when the user chooses a file or folder, it grants your application access to the specified location and then returns information about the user’s selection to you.

Here’s an example of how you can get access to a folder that the user asks for:

let panel = NSOpenPanel()

panel.canChooseDirectories = true

panel.canChooseFiles = false

panel.beginWithCompletionHandler() {

(result : Int) in

let theURL = panel.URL

// Do something with the URL that the user selected;

// we now have permission to work with this location

}

Security-Scoped Bookmarks

One downside to this approach of asking for permission to access files is that the system will not remember that the user granted permission. It’s a potential security hole to automatically retain permissions for every file the user has ever granted an app access to, so OS X instead provides the concept of security-scoped bookmarks. Security-scoped bookmarks are like the bookmarks in your web browser, but for files; once your application has access to a file, you can create a bookmark for it and save it. On application launch, your application can load the bookmark and have access to the file again.

There are two kinds of security-scoped bookmarks: app-scoped bookmarks, which allow your application to retain access to a file across launches, and document-scoped bookmarks, which allow your app to store the bookmark in a file that can be given to another user on another computer. In this book, we’ll be covering app-scoped bookmarks.

To use security-scoped bookmarks, you need to explicitly indicate that your app uses them in its entitlements file. This is the file that’s created when you turn on the Enable Entitlements option: it’s the file with the extension .entitlements in your project. To enable app-scoped bookmarks, you open the Entitlements file and add the following entitlement: com.apple.security.files.bookmarks.app-scope. Set this entitlement to YES.

You can then create a bookmark file and save it somewhere that your application has access to. When your application later needs access to the file indicated by your user, you load the bookmark file and retrieve the URL from it; in doing this, your application will be granted access to the location that the bookmark points to.

To create and save bookmark data, you do this:

// Get the location in which to put the bookmark;

// documentURL is determined by asking the NSFileManager for the

// user's documents folder; see earlier in this chapter

var bookmarkStorageURL =

documentURL.URLByAppendingPathComponent("savedbookmark.bookmark")

// selectedURL is a URL that the user has selected using an NSOpenPanel

let bookmarkData = selectedURL.bookmarkDataWithOptions(

NSURLBookmarkCreationOptions.WithSecurityScope,

includingResourceValuesForKeys: nil, relativeToURL: nil, error: nil)

// Save the bookmark data

bookmarkData?.writeToURL(bookmarkStorageURL, atomically: true)

To retrieve a stored bookmark, you do this:

let loadedBookmarkData = NSData(contentsOfURL: bookmarkStorageURL)

var loadedBookmark : NSURL? = nil

if loadedBookmarkData?.length > 0 {

var isStale = false

var error : NSError? = nil

loadedBookmark = NSURL(byResolvingBookmarkData:loadedBookmarkData!,

options: NSURLBookmarkResolutionOptions.WithSecurityScope,

relativeToURL: nil, bookmarkDataIsStale: nil, error: nil)

// We can now use this file

}

When you want to start accessing the file pointed to by the bookmarked URL, you need to call startAccessingSecurityScopedResource on that URL. When you’re done, call stopAccessingSecurityScopedResource.

You can find a full working project that demonstrates this behavior in this book’s source code.

iCloud

Introduced in iOS 5, iCloud is a set of technologies that allow users’ documents and settings to be seamlessly synchronized across all the devices that they own.

iCloud is heavily promoted by Apple as technology that “just works”—simply by owning a Mac, iPhone, or iPad, your documents are everywhere that you need them to be. In order to understand what iCloud is, it’s worth taking a look at Apple’s advertising and marketing for the technology. In the ads, we see users working on a document, and then just putting it down, walking over to their Macs, and resuming work. No additional effort is required on the part of the user, and users are encouraged to think of their devices as simply tools that they use to access their omnipresent data.

This utopian view of data availability is made possible by Apple’s growing network of massive data centers, and by a little extra effort on the part of you, the developer.

NOTE

iCloud also supports syncing Core Data databases. However, Core Data and iCloud syncing is a huge issue, and implementing and handling this is beyond what we could cover in this chapter. If you’re interested in learning more about this, take a look at Marcus S. Zarra’s excellent Core Data, 2nd Edition (Pragmatic Bookshelf).

In this chapter, you’ll learn how to create applications that use iCloud to share settings and documents across the user’s devices.

What iCloud Stores

Simply put, iCloud allows your applications to store files and key-value pairs on Apple’s servers. Apps identify which file storage container or key-value pair database they want to access, and the operating system takes care of the rest.

In the case of files, your application determines the location of a container folder, the contents of which are synced to the network. When you copy a file into the container or update a file that’s already in the container, the operating system syncs that file across all other applications on devices that have access to the same container.

For settings, you access an instance of NSUbiquitousKeyValueStore, which works almost identically to NSUserDefaults with the exception that it syncs to all other devices.

The word “ubiquitous” appears a lot when working with iCloud. So often, in fact, that it’s used instead of the marketing term “iCloud.” This is intended to reinforce what iCloud should be used for—it’s not just a storage space on the Internet, like Box.net or similar “cloud file storage” services, but rather a tool for making users’ data ubiquitous, so they can access it from anywhere.

Users are limited in the amount of data they can store. By default, iCloud users get 5 GB of space for free, and can pay for more. There aren’t any per-application limits on the amount of data that your application can store, but the user isn’t allowed to exceed their total limit (though they can purchase more space). For the key-value store, you can store 64 KB of information per application.

This means that when you’re working out how iCloud fits into your application, you have to choose where you’re going to put the data. Storing files in an iCloud container is a good option if your application works with documents—image editors or word processors are good examples. Files are also useful for storing more structured information, such as to-do lists or saved game files. If you want to store simple, application-wide state, such as the most recently opened document, then the key-value store works well.

More than one application can access the same iCloud container or key-value store. All that’s required is that the same developer writes both, and that the bundle IDs have the same team prefix.

NOTE

iCloud works on both the Mac, iOS devices, and on the iOS Simulator. However, when you’re developing using the iOS Simulator, you need to manually indicate when iCloud should synchronize the local data with the data stored on the server.

You do this by opening the Debug menu, and choosing Trigger iCloud Sync. You can also do this by pressing Command-Shift-I.

Setting Up for iCloud

In order to use any of Apple’s online services, an application needs to identify itself and the developer who created it. This means that if you want to work with iCloud, you must have a paid developer account for each platform that you want to develop on. So if you want to make iCloud apps for the Mac, you need a paid Mac developer account. And if you want to make iOS apps at all, of course, you need a paid iOS developer account.

To get started, we’ll create an application in Xcode, and then configure it so that it has access to iCloud:

1. Create a new Cocoa application and name it Cloud. When you create the application, write down the application’s bundle identifier somewhere.

2. When Xcode has finished creating the application, select the project at the top of the project navigator. In the application’s Capabilities tab, scroll down to iCloud.

3. Turn on the switch next to iCloud. Xcode will begin configuring the application, by creating files that indicate that the app needs access to iCloud, as well as by letting Apple know that the app needs access.

4. Turn on the “Key-value storage” checkbox, as well as the “iCloud documents” checkbox.

To access iCloud storage and store files, you indicate which iCloud container your application should use. The examples in this chapter will cover both the key-value store and iCloud file containers, and both are identified with the same style of identifier. For now, leave the Container setting as “Use default container”:

NOTE

By default, Xcode configures your application to use an iCloud container with the same identifier as your application. If you have multiple apps that should share data through iCloud, they need to be configured to use the same iCloud container. This will become important later in the chapter, when we create an iOS app that uses iCloud.

The application is now set up to use iCloud. To get started working with the system, we’ll first make sure that everything’s working as it’s supposed to.

Testing Whether iCloud Works

In order to determine whether the application has access to iCloud, we’ll run a quick test to make sure that our setup is working. To do this, we’ll ask the NSFileManager class for the “ubiquity container” URL. This is the location on the filesystem that is used for storing iCloud files; if the system returns the URL, we’re in business. If it returns nil, then the app hasn’t been set up for iCloud properly.

NOTE

The ability of the app to access the ubiquity container isn’t affected by the device’s ability to talk to the iCloud servers. If you’re offline, you can store information in iCloud—it just won’t get synced until you’re back online.

To add the test, replace applicationDidFinishLaunching in AppDelegate.swift with the following code:

// We run this on a new background queue because it might take some time

// for the app to determine this URL

let backgroundQueue = NSOperationQueue()

backgroundQueue.addOperationWithBlock() {

// Pass 'nil' to this method to get the URL for the first iCloud

// container listed in the app's entitlements

let ubiquityContainerURL = NSFileManager.defaultManager()

.URLForUbiquityContainerIdentifier(nil)

println("Ubiquity container URL: \(ubiquityContainerURL)")

}

Run the application. Take a look at the console output. If you see a URL, then iCloud is configured correctly. If the app reports that the ubiquity container URL is nil, then iCloud isn’t set up correctly, and you should double-check your code signing and settings.

Storing Settings

The first thing that we’ll do is use the key-value store to cause a setting to be stored in iCloud, which will be accessible via both an iOS application and a Mac application.

The key-value store, accessed via NSUbiquitousKeyValueStore, works very much like the NSUserDefaults object. You can store strings, numbers, arrays, and dictionaries in the store. As we mentioned before, the total amount of data that your app can store in the key-value store is 64KB, and each item can be no larger than 64KB.

In this example, we’re going to start by storing a single string in iCloud. First, we’ll update the AppDelegate object to store and retrieve this value from the key-value store. To accomplish this, open AppDelegate.swift and add the following computed property to the AppDelegate class:

var cloudString : String? {

get {

return NSUbiquitousKeyValueStore

.defaultStore()

.stringForKey("cloud_string")

}

set {

NSUbiquitousKeyValueStore.defaultStore()

.setString(newValue, forKey: "cloud_string")

NSUbiquitousKeyValueStore.defaultStore().synchronize()

}

}

The getter of this computed property retrieves the value from the ubiquitous key-value store, while the setter stores the value and then immediately syncs the in-memory local copy to the disk. This also indicates to iCloud that there are new changes to push up.

WARNING

When you synchronize the key-value store, your changes aren’t immediately uploaded to iCloud. In fact, iCloud limits the upload rate to several per minute.

This means that you can’t assume that your changes will appear immediately—iCloud might decide to just wait a while before sending your changes. This is on top of any delays caused by the network.

Handling External Changes

This method here works fine when we’re the only application accessing the data, but the whole point of iCloud is that it’s designed for multiple applications accessing the same data. It’s therefore possible that, while the application is running, another instance of the application (perhaps running on another device that the user owns) changes the same value. Our application needs to know that the change has taken place, so that both apps show the same information.

When the key-value store is changed externally (i.e., by another application) the notification NSUbiquitousKeyValueStoreDidChangeExternallyNotification is posted. So to be informed of these changes, we’ll make the AppDelegate class receive this notification, and then let the rest of the application know that the cloudString property changed (which will in turn make the UI update).

First, we’ll add a property that stores the observer for the notifications, and then we’ll register to run a block when the key-value store is changed:

1. Add the following property to the AppDelegate class:

var storeChangeObserver : AnyObject? = nil

2. Add the following method to AppDelegate.swift:

3. storeChangeObserver =

4. NSNotificationCenter.defaultCenter()

5. .addObserverForName(

6. NSUbiquitousKeyValueStoreDidChangeExternallyNotification,

7. object: self,

8. queue: NSOperationQueue.mainQueue()) {

9. (notification) in

10. self.willChangeValueForKey("cloudString")

11. self.didChangeValueForKey("cloudString")

}

We’re now done with the code. It’s time to create the interface, which will consist of a text field that’s bound to the application delegate’s cloudString property. This way, whenever the user changes the contents of the text field, the setter for the cloudString property will be run, which stores the new string in iCloud. Additionally, because the store change observer calls willChangeValueForKey and didChangeValueForKey, the text field will automatically get updated when new data is received from iCloud:

3. Open MainWindow.xib, and open the window.

4. Drag in an NSTextField.

5. With the text field selected, open the Bindings Inspector, which is the second tab to the right at the top of the Utilities pane.

6. Open the Value property and choose App Delegate in the “Bind to” drop-down menu. Set the Model Key Path to self.cloudString.

We’re all set—the interface is prepared and will show the value stored in iCloud. Go ahead and run the app: you can enter text, and it will be saved.

The iOS Counterpart

This is all well and good, but iCloud only gets impressive when there’s more than one device that has access to the same information. We’ll now create an iOS application that shows what you type in the Mac application; the app will also allow you to make changes, which will automatically show up in the Mac app.

NOTE

You can develop iCloud applications on both the iOS Simulator as well as a real device. In either case, you need to make sure your device is signed into an iCloud account—your phone is probably signed into one already, but your iOS Simulator almost definitely isn’t.

Signing into the simulator is easy, though: simply go to the Settings application, scroll down to “iCloud,” and sign in, just as you would on a real phone.

To keep the project manageable, we’re going to make the iOS app be an additional part of the Mac app’s project. Instead of creating a new project, we’ll create a new target for the iOS app. This will keep everything in the same window, and has some additional advantages like making it easier to share source code between the two apps:

1. Create a new target by choosing File→New→Target. Name it Cloud-iOS.

2. Make the new target a single view iOS application. Make sure the bundle identifier is the same as the app ID that you just created for the iOS application.

Once the project is created, the application needs to be configured to use iCloud, just like the Mac application. Specifically, the iOS app must be configured to use the same iCloud resources as the Mac app, which will make it possible for the two apps to share data. Here are steps you’ll need to follow:

1. Select the project at the top of the project navigator. Select the iOS application that you just added. Open the Capabilities tab.

2. Scroll down to the iCloud section and turn on the switch.

3. Turn on the “Key-value storage” checkbox, as well as the “iCloud documents” checkbox.

The iOS application needs to access the same key-value store, as well as the same iCloud documents container. To set this up, follow these steps:

1. Change the Containers option to “Specify custom containers.” Then, make sure that only one iCloud container is selected (i.e., the container used by the OS X application).

2. Next, open the .entitlements file, which Xcode should have just created for you. Change the iCloud Key-Value Store setting to read $(TeamIdentifierPrefix) followed by the bundle ID of your OS X application. This will ensure that both apps are accessing the same key-value store.

The iOS application is now ready to work with iCloud, just like the Mac app. We’ll now set up its interface, which will consist of a single text field.

In order to be notified of when the user is done editing, we’ll make the view controller used in this iOS application a UITextFieldDelegate. When the user taps the Return key (which we’ll convert to a Done button), the application will store the text field’s contents in iCloud:

1. Open Main.storyboard. Drag in a UITextField and place it near the top of the screen.

2. Select the text field and open the Attributes Inspector. Scroll down to the “Return key” drop-down item, and choose Done.

The interface is done, but we still need to make it so the view controller is notified when the user taps the Done button:

1. Control-drag from the text field to the view controller, and choose “delegate” from the pop-up menu that appears.

2. Open ViewController.swift in the inspector.

3. Control-drag from the text field into the ViewController class. Create a new outlet called textField.

4. Make the class conform to UITextFieldDelegate by changing its class definition line to look like this:

class ViewController: UIViewController, UITextFieldDelegate {

Now we can make the application draw its data from iCloud. We’ll do this by setting the text of the textField to whatever’s in the iCloud key-value store when the view loads.

We’ll also register the ViewController class as one that receives notifications about when the key-value store is updated externally.

NOTE

iCloud updates its contents both when the application is running and when it’s not. This means that if you make a change to a setting in the key-value store on your iPhone and then later open the same app on your iPad, the data may have already arrived.

1. Add the following property to the ViewController class:

var keyValueStoreDidChangeObserver : AnyObject?

2. Next, add the following code to the viewDidLoad method:

3. self.textField.text =

4. NSUbiquitousKeyValueStore.defaultStore()

5. .stringForKey("cloud_string")

6.

7. keyValueStoreDidChangeObserver = NSNotificationCenter.defaultCenter()

8. .addObserverForName(

9. NSUbiquitousKeyValueStoreDidChangeExternallyNotification,

10. object: nil, queue: NSOperationQueue.mainQueue()) {

11.

12. (notification) in

13.

14. self.textField.text =

15. NSUbiquitousKeyValueStore.defaultStore()

16. .stringForKey("cloud_string")

17.

}

Next, we’ll add the method that runs when the user taps the Done button. This works because the class is the delegate of the text field; textFieldShouldReturn is called by the text field to find out what happens when the Return button is tapped.

In this case, we’ll make the keyboard go away by making the text field resign first-responder status, and then store the text field’s contents in iCloud.

Add the following method to ViewController:

func textFieldShouldReturn(textField: UITextField!) -> Bool {

self.textField.resignFirstResponder()

NSUbiquitousKeyValueStore.defaultStore(

).setString(self.textField.text,

forKey: "cloud_string")

return false;

}

We can now see this in action! Run the iOS app and the Mac app together. Change a value on one of the apps and watch what happens.

NOTE

Be patient—it might take a few seconds before the change appears on the other device.

iCloud Storage

Storing keys and values in iCloud is extremely useful for persisting user preferences across all their devices, but if you want to make the user’s files just as ubiquitous through iCloud, you need to use iCloud storage.

In this section, we’ll make an app that allows the user to store stuff in iCloud storage. The Mac app will let you add items to iCloud and list everything in storage. Its iOS counterpart will be simpler and show a list of files currently in iCloud storage, which updates as files are added or removed.

iCloud Storage on OS X

Before we can get to work, we need to give the Mac application permission to access the user’s files. By default, when you enable iCloud, Xcode helpfully marks the application as sandboxed.

Sandboxing an application restricts what it’s allowed to access. Before sandboxing, all applications were allowed to access any file that belonged to the user, which caused problems if the app was compromised by a remote attacker. Apple requires any application that’s submitted to the Mac App Store to be sandboxed. (All iOS applications are sandboxed—it’s a requirement of running on the device.)

By default, the application will be sandboxed to the point where it can’t access any user files at all. Because we’re making an application that lets the user take files and move them into iCloud, we’ll need access to those files:

1. Open the project settings for the Mac application and open the Capabilities tab.

2. Scroll to the App Sandbox section, and turn on the switch next to it. Next, change the User Selected File setting from “None” to “Read/Write” (you’ll find this setting in the File Access section).

With that out of the way, we can begin working with iCloud storage in the Mac app.

The way that our implementation will work is this: we’ll have a property on the AppDelegate class that is an array containing NSURLs of each path of the files in the storage container. This array will be displayed in a table view so that you can see what’s included. We’ll also add a button that, when clicked, will prompt the user for a file to move into iCloud storage.

In real life, you’d likely do something more interesting with the files than just show that they’re there, but this will get us started.

Open AppDelegate.swift and add the following code to the AppDelegate class:

dynamic var filesInCloudStorage : [NSURL] = []

NOTE

Marking a property as dynamic allows Cocoa bindings to work with it.

Next, we’ll create and set up the table view that displays the list of files. To keep things simpler, we’ll use bindings to make the table view show its content:

1. Open MainWindow.xib and drag in an NSTableView into the main window. Make it fill the rest of the window.

2. Select the table view and make it have one column.

3. Drag in an NSButton and change its label to Add File....

With the interface laid out, we can begin to connect it to the code. We’ll start by making the button run an action method when clicked, and then bind the table view to the application. Because we’re working with an array, we’ll use an array controller to manage the link between the array of files stored in the app delegate and the table view:

1. Open AppDelegate.swift in the assistant.

2. Control-drag from the button into AppDelegate and create a new action called addFile.

We’ll now bind the table to the app delegate via an array controller:

1. Drag an array controller into the outline.

2. Select the array controller and open the Bindings tab.

3. Open the Content Array property, and choose App Delegate from the “Bind to” drop-down menu.

4. Set the Model Key Path to self.filesInCloudStorage.

5. Select the table view column, and bind its Value to the Array Controller. Set the controller key to arrangedObjects and the model key path to description.

We’re using description because it’s a convenient way to simply display a string version of the contents of the array.

Next, we need to load the list of files that are in iCloud into the array, and then keep an eye out for new things arriving. To check the contents of the iCloud container, we first get its URL with this code:

let documentsDirectory = NSFileManager.defaultManager()

.URLForUbiquityContainerIdentifier(nil)?

.URLByAppendingPathComponent("Documents", isDirectory: true)

The ubiquity container is the folder that contains all of the information that’s synced to iCloud. Inside this is another folder called Documents, which is where your application should put all synced documents. It’s technically possible to store information outside this folder, but the advantage of using the Documents folder is that, on iOS, the user can delete individual documents in order to free space, whereas anything outside that folder is considered internal data and can’t be individually deleted by users—they can only remove it by deleting the entire iCloud container.

To work out what’s inside the iCloud container and to be informed of when its contents change, we use the NSMetadataQuery class. This class, once configured with information about what you’re looking for, runs continuously and sends notifications whenever its contents change.

To use this, we’ll add an instance variable to store the query object, and when the application launches, we’ll configure and start the query:

1. Open AppDelegate.swift and add the following properties to the class:

2. var metadataQuery : NSMetadataQuery!

3. var metadataQueryDidUpdateObserver : AnyObject?

var metadataQueryDidFinishGatheringObserver : AnyObject?

4. Add the following code to the end of the applicationDidFinishLaunching method:

5. metadataQuery = NSMetadataQuery()

6. metadataQuery.searchScopes =

7. [NSMetadataQueryUbiquitousDocumentsScope]

8. metadataQuery.predicate =

9. NSPredicate(format: "%K LIKE '*'", NSMetadataItemFSNameKey)

10.

11.self.metadataQueryDidUpdateObserver =

12. NSNotificationCenter.defaultCenter()

13. .addObserverForName(NSMetadataQueryDidUpdateNotification,

14. object: nil, queue: NSOperationQueue.mainQueue()) {

15.

16. (notification) in

17. self.queryDidUpdate()

18.}

19.

20.self.metadataQueryDidFinishGatheringObserver =

21. NSNotificationCenter.defaultCenter()

22. .addObserverForName(NSMetadataQueryDidFinishGatheringNotification,

23. object: nil, queue: NSOperationQueue.mainQueue()) {

24.

25. (notification) in

26. self.queryDidUpdate()

27.}

28.

metadataQuery.startQuery()

This code starts by creating the metadata query object, and instructs it to limit its results to only include items found inside the Documents folder in the ubiquity container. We also give it a predicate, which is a description of what to look for—in this case, we’re saying “find all objects whose filenames are anything,” which translates to “all files in the Documents folder.”

We then register the app delegate to receive notifications whenever the metadata finishes its initial sweep of the folder, and also whenever the folder changes contents. In both cases, the same method will be called.

Finally, the query is started.

We now need to add the queryDidUpdate method, which will prepare the filesInCloudStorage property and fill it with the paths that it found.

Add the following method to AppDelegate:

func queryDidUpdate() {

var urls : [NSURL] = []

for item in metadataQuery.results {

if let metadataItem = item as? NSMetadataItem {

let url =

metadataItem

.valueForAttribute(NSMetadataItemURLKey) as NSURL

urls.append(url)

}

}

self.filesInCloudStorage = urls

}

This code loops over every result in the query and retrieves the path for it. The paths are then stored in an array, which is used to update the filesInCloudStorage property. Because we’re using bindings, the act of updating this property will update the contents of the table view.

Next, we add the method that actually adds an item to iCloud storage. This method presents a file-open panel that lets the user choose which item to move into storage.

The process of moving a file into storage is the following. First, you work out the URL of the file you want to move. Then, you ask the NSFileManager to generate a destination URL. Finally, you perform the move by asking the file manager to make the file ubiquitous, passing in the source and destination URLs.

NOTE

Moving files into storage is just that—moving the file. If you want to copy a file into storage, duplicate it and move the copied file.

Add the following method to AppDelegate:

@IBAction func addFile(sender : AnyObject?) {

let panel = NSOpenPanel()

panel.beginSheetModalForWindow(self.window) {

(result) in

if (result == NSOKButton) {

let containerURL = NSFileManager.defaultManager()

.URLForUbiquityContainerIdentifier(nil)?

.URLByAppendingPathComponent("Documents",

isDirectory: true)

if let sourceURL = panel.URL {

let destinationURL = containerURL?

.URLByAppendingPathComponent(

sourceURL.lastPathComponent)

var error : NSError?

// Move the file into iCloud (AKA "ubiquitous storage")

NSFileManager.defaultManager().setUbiquitous(true,

itemAtURL: sourceURL,

destinationURL: destinationURL!,

error: &error)

if error != nil {

println("Couldn't make the file ubiqitous: \(error)")

}

}

}

}

}

Run the app. You can now add stuff to iCloud.

iCloud Storage on iOS

Now we’ll make the same thing work on iOS. First, we’ll update the UI to include a text field that displays the list of files, and then we’ll add the same NSMetadataQuery that lets the app know what’s in the container:

1. Open the main storyboard and add a text view. Make it not editable.

2. Open ViewController.swift in the assistant.

Control-drag from the text field into the ViewController class section, and connect it to a new outlet called fileList.

Now we’ll make the code work. This is almost identical to the Mac version—we create the metadata query, prepare it, and set it running. When the query finds files, we’ll update the text field and display the list of items.

We first need to create properties that store the NSMetadataQuery, along with observers for notifications that will fire when the contents of the iCloud container get updated.

1. Add the following properties to the ViewController class:

2. var queryDidFinishGatheringObserver : AnyObject?

3. var queryDidUpdateObserver: AnyObject?

var metadataQuery : NSMetadataQuery!

2. Next, add the following code to the end of the viewDidLoad method:

3. metadataQuery = NSMetadataQuery()

4. metadataQuery.searchScopes =

5. [NSMetadataQueryUbiquitousDocumentsScope]

6. metadataQuery.predicate = NSPredicate(format: "%K LIKE '*'",

7. NSMetadataItemFSNameKey)

8.

9. queryDidUpdateObserver = NSNotificationCenter.defaultCenter()

10. .addObserverForName(NSMetadataQueryDidUpdateNotification,

11. object: metadataQuery,

12. queue: NSOperationQueue.mainQueue()) {

13. (notification) in

14.

15. self.queryUpdated()

16. }

17.

18. queryDidFinishGatheringObserver = NSNotificationCenter.defaultCenter()

19. .addObserverForName(NSMetadataQueryDidFinishGatheringNotification,

20. object: metadataQuery,

21. queue: NSOperationQueue.mainQueue()) {

22. (notification) in

23.

24. self.queryUpdated()

25. }

26.

27. metadataQuery.startQuery()

28.

self.fileList.text = ""

This code is pretty much identical to the Mac version. The only difference is that we’re directly updating the text view instead of using bindings.

Next, we’ll add the method that updates when the metadata query finds files.

Add the following method to ViewController:

func queryUpdated() {

var files : [NSURL] = []

for item in metadataQuery.results {

if let metadataItem = item as? NSMetadataItem {

let url =

metadataItem

.valueForAttribute(NSMetadataItemURLKey) as NSURL

files.append(url)

}

}

self.fileList.text = files.description

}

Now run the app, and add a file to iCloud via the Mac app. It’ll appear in the iOS app.

It’s important to note that items that show up in the iCloud container aren’t necessarily fully downloaded, particularly if the file is large. Likewise, an item that’s uploading to iCloud might take some time.

You can determine the status of a file at a given URL by using NSURL’s valueForAttribute method. For example, to work out if a file is completely available, you do this:

// metadataItem is an NSMetadataItem describing an item in the

// ubiquity container

var downloadStatus =

metadataItem.valueForAttribute(

NSMetadataUbiquitousItemDownloadingStatusKey) as NSString

if downloadStatus == NSMetadataUbiquitousItemDownloadingStatusDownloaded {

// it's downloaded!

}

Document Pickers

Document pickers are an iOS feature that allow the user to select files, either from iCloud or from an external provider (such as a cloud file storage service). Document pickers allow your app to work with any files that the user wants to access, including files stored in other applications.

NOTE

Document pickers are a fairly large topic, and we don’t have room to go into them in very much detail here. We’ll be covering how to set up a document picker, and how to configure it to let the user select a file, and to make your app create a copy of that file for its own use.

However, document pickers can go beyond this. For example, you can make a document picker that opens a file that’s stored in another app’s container, make changes, and save it back to that container. For more information on how to do this, see the Document Picker Programming Guide, located in the Xcode documentation.

A document picker works like this: the user lets your app know that she wants to access a document, and your app creates and configures a UIDocumentPickerViewController, and then presents it. The user navigates to the file they want—which may be inside another application’s container—and selects it. Once that happens, your app is granted temporary permission to access that file, and can do some work with it. This work can include things like making a copy of the file, or opening the file and presenting it to the user for editing.

When you create a UIDocumentPickerViewController, you specify two things:

§ The types of the files you want to select

§ What mode the picker should operate in

The type of the file is a Uniform Type Identifier, which is a standard string that defines file types. For example, public.text means any file that contains text, while org.gnu.gnu-zip-archive means a gzipped archive. To indicate what kinds of files you want the user to select, you provide an array of these strings.

The mode of the document picker defines what the app intends to do with the file that the user has selected. There are four available modes:

Open

The app intends to open the file for editing, and will put it back when changes are done.

Move

The app intends to move the file from its current location into the app’s container.

Import

The app intends to make a new copy of the file, and store that copy in its container.

Export

The app has a new file to place in a third-party storage provider.

This chapter discusses the import mode, because it’s the easiest and most direct way to get data into your app. However, the other three modes have their own uses, and you should check out the documentation to see more about how they’re used.

NOTE

Before we get started in building an app that uses document pickers, it’s good to ensure that there’s actually content in iCloud for you to use.

To quickly add a file to iCloud, follow these steps:

1. Open the TextEdit application on your Mac.

2. Write some text—go on, it can be anything.

3. Save the file. When prompted for a location, choose iCloud. The file will now be stored in iCloud storage.

To implement document pickers, we first need to add a button that triggers the appearance of one:

1. Open your iOS application’s storyboard. Add a button to the screen, and make its label read Select File.

2. Connect the button to a new action method in ViewController, called selectFile.

3. Add the following property to the ViewController class:

var documentSelector : UIDocumentPickerViewController?

4. Make the ViewController class conform to the UIDocumentPickerDelegate protocol, by adding it to the list of protocols at the top of the class.

When the button is tapped, we need to create and configure the document picker, and then present it to the user.

5. Implement the selectFile method using the following code:

6. @IBAction func selectFile(sender: UIButton) {

7.

8. // We want to select any file; 'public.data' means 'any data'

9. documentSelector = UIDocumentPickerViewController(documentTypes:

10. ["public.data"],

11. inMode: UIDocumentPickerMode.Import)

12. documentSelector?.delegate = self

13.

14. // Show the picker

15. self.presentViewController(documentSelector!,

16. animated: true,

17. completion: nil)

18.

}

The public.data type identifier means any item that has data—in other words, any file. (This type excludes folders, which don’t actually contain data themselves—they’re just containers.)

When the user selects a file, the document picker contacts its delegate, and provides it with an NSURL object that points to the file that the user selected.

What you do with this NSURL depends on the mode of the document picker. In the case of importing a file, the file pointed to is a temporary file, which will go away when the app closes. In this example, we want to copy any file that the user selects into the app’s iCloud container. This means that we need to first copy the file into a temporary position, and then use the NSFileManager class to move that copy into ubiquitous storage.

There’s one potential snag, though—if the file that was selected happens to be inside another app’s iCloud container, the app must first indicate to the system that it wants permission to access the file. You do this using the startAccessingSecurityScopedResource andstopAccessingSecurityScopedResource methods, which were discussed in Security-Scoped Bookmarks.

When you’re done working with the URL, you then need to dismiss the document picker.

There’s one last thing to note: if the user doesn’t select a file, and instead dismisses the picker without picking anything, your application also needs to handle this. Usually, all you need to do is to just dismiss the picker, but you might have some additional logic that you need to run. Add the following methods to ViewController.swift:

func documentPicker(controller: UIDocumentPickerViewController,

didPickDocumentAtURL url: NSURL) {

// This picker was set up to use the UIDocumentPickerMode.Import mode

// That means that 'url' points to a temporary file that we can move

// into our container

// Let the system know that we're about to start using this—it might

// be in some other app's container, which means we need to get the

// system to temporarily unlock it

url.startAccessingSecurityScopedResource()

let fileName = url.lastPathComponent

// Copy it to a temporary location

let temporaryURL = NSURL.fileURLWithPath(NSTemporaryDirectory(),

isDirectory:true)?

.URLByAppendingPathComponent(fileName)

var copyError : NSError? = nil

NSFileManager.defaultManager().copyItemAtURL(url,

toURL: temporaryURL!, error: &copyError)

if let theError = copyError {

println("Error copying: \(theError)")

}

// We're done—let the system know that we don't

// need access permission anymore

url.stopAccessingSecurityScopedResource()

// Now, move that item into ubiquitous storage

let destinationURL = NSFileManager.defaultManager()

.URLForUbiquityContainerIdentifier(nil)?

.URLByAppendingPathComponent("Documents")

.URLByAppendingPathComponent(fileName)

var makeUbiquitousError : NSError? = nil

NSFileManager.defaultManager().setUbiquitous(true,

itemAtURL: temporaryURL!,

destinationURL: destinationURL!,

error: &makeUbiquitousError)

if let theError = makeUbiquitousError {

println("Error making ubiquitous: \(theError)")

}

// Finally, dismiss the view controller

self.dismissViewControllerAnimated(true, completion: nil)

}

func documentPickerWasCancelled(controller: UIDocumentPickerViewController!)

{

// Nothing got selected, so just dismiss it

self.dismissViewControllerAnimated(true, completion: nil)

}

You’re all done. Run the application, and tap the Select File button. Find the file you want to add, and select it. If you have the OS X application running as well, you’ll see the file you just added on your iOS device appear in the file list.

Using iCloud Well

In order to be a good citizen in iCloud, there are a number of things that your application should do in order to provide the best user experience:

§ Don’t store some documents in iCloud and some outside. It’s easier for users to choose to store all their data in iCloud or to not store anything there.

§ Only store user-created content in iCloud. Don’t store caches, settings, or anything else—iCloud is meant for storing things that cannot be re-created by the app.

§ If you delete an item from iCloud, the file is removed from all the user’s devices and computers. This means that you should confirm a delete operation with the user before performing it, as users might not understand the implications and may think that they’re only deleting the local copy of the file.