Tải bản đầy đủ
Chapter 10. iCloud and Data Storage

Chapter 10. iCloud and Data Storage

Tải bản đầy đủ

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 NSUser
Defaults class. To get one, you must ask the NSUserDefaults class for the
standardUserDefaults:
let defaults = NSUserDefaults.standardUserDefaults()

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.
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
]

210

|

Chapter 10: iCloud and Data Storage

// Provide this dictionary to the defaults object
defaults.registerDefaults(myDefaults)

Once this is done, you can ask the defaults object for values.
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” on page 211) 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” on page 58 in
Chapter 2).
Additional methods exist for retrieving values from an NSUser
Defaults object. For more information, see the Preferences and Set‐
tings 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).
Preferences

|

211

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 stor‐
age 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” on page 220.
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 else‐
where on the disk.
Files that are stored in the application’s bundle are kept inside the .app folder and dis‐
tributed 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’s Resources folder, while on iOS, it is copied into the root
folder of the bundle.

212

|

Chapter 10: iCloud and Data Storage

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 docu‐
ments aren’t stored in this location.
If a file is stored in the application bundle, it’s part of the codesigning 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” on page 67. This
chapter covers how to work with files that are stored elsewhere.
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 im‐
ages 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.
Sandboxes, which are discussed later in this chapter in “Working with
the Sandbox” on page 217, 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 work‐
ing 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 appli‐
cation 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:
Working with the Filesystem

|

213

let fileManager = NSFileManager.defaultManager()

NSFileManager allows you to set a delegate on it, which receives mes‐

sages 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

214

|

Chapter 10: iCloud and Data Storage

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 NSFile
Manager 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.
Files in a temporary directory are subject to deletion without warn‐
ing. If the operating system decides it needs more disk space, it will
begin deleting the contents of temporary directories. Don’t put any‐
thing 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
}

Working with the Filesystem

|

215

Note that you can pass in an NSDictionary containing the desired attributes for the new
directory.
If you set a YES value for the withIntermediateDirectories param‐
eter, 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 cre‐
ate 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)

There’s no undo for removing files or folders using NSFile
Manager. 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)

216

|

Chapter 10: iCloud and Data Storage

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 di‐
rectory 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 URL
ByAppendingPathComponent:
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 con‐
tainer. 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.
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.

Working with the Sandbox

|

217

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.
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 han‐
dles 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
}

218

| Chapter 10: iCloud and Data Storage

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 appli‐
cation 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 book‐
marks, 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 {

Working with the Sandbox

|

219

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.
iCloud also supports syncing Core Data databases. However, Core
Data and iCloud syncing is a huge issue, and implementing and han‐
dling 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. Zar‐
ra’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.

220

|

Chapter 10: iCloud and Data Storage