Doc, You Meant Storage - Learn iOS 8 App Development, Second Edition (2014)

Learn iOS 8 App Development, Second Edition (2014)

Chapter 19. Doc, You Meant Storage

If you want your iOS app to store more than a few tidbits of information, you need documents. iOS provides a powerful document framework that brings data storage into the 21st century. The iOS document (UIDocument) class takes care of, or lets you easily implement, modern features such as autosaving, versioning, and cloud storage. In the process, you’ll finally learn how to archive objects. In this chapter, you will do the following:

· Create a custom document object

· Use a document object as your app’s data model

· Learn how to archive and unarchive your data model objects

· Design a document that can be loaded or saved incrementally

· Handle asynchronous document loading

· Manage document changes and autosaving

You’ll find numerous “how-to” guides for using UIDocument because, quite frankly, it’s a complicated class to use. There are a lot moving parts and more than a few details you must pay attention to. This has led many developers to ignore UIDocument and “roll their own” document storage solution. I beg you not to do that. Conquering UIDocument isn’t that hard, and the rewards are substantial.

UIDocument can seem overwhelming—until you understand why UIDocument works the way it does. Once you understand some of the reasoning behind its architecture and what it’s accomplishing for you, the code you need to write will all make sense. So in this chapter, you’ll concentrate not on just the how but the why. By the end, you’ll be using UIDocument like a pro.

Document Overview

The word document has many meanings, but in this context a document is a data file (or package) containing user-generated content that your app opens, modifies, and ultimately saves to persistent storage. We’re all used to documents on desktop computer systems. In mobile devices, the concept of a document takes a backseat, but it’s still there and in much the same form. In a few apps, like the Pages word processing app, the familiar document metaphor appears front and center. You launch the app, see a collection of your named documents, and choose one to work on; the document opens, and you begin typing. In other apps, it’s not as clear that you’re using individual documents, and some apps hide the mechanics of documents entirely. You can choose any of these approaches for your app. iOS provides the tools needed for whatever interface you want, but it doesn’t dictate one.

This flexibility lets you add document storage and management to your app completely behind the scenes, loosely coupled to your user interface, or echoing the legacy document metaphor of desktop computer systems. Whatever you decide to do with your interface, the place to start is theUIDocument class. Here are the basic steps to using UIDocument in your app:

1. Create a custom subclass of UIDocument.

2. Design an interface for choosing, naming, and sharing documents (optional).

3. Convert your app’s data model to and from data that’s suitable for permanent storage.

4. Handle asynchronous reading of documents.

5. Move documents into the cloud (optional).

6. Observe change notifications from shared documents and handle conflicts (optional).

7. Implement undo/redo capabilities, or at least track changes to a document.

You’re going to revisit the MyStuff app and modify it so it stores all of those cool items, their descriptions, and even their pictures, in a document. There are no interface changes to MyStuff this time. The only thing your users will notice is that their stuff is still there when they relaunch your app!

Where, Oh Where, Do My Documents Go?

So, where do you store documents in iOS? Here’s the short answer: Store your documents in your app’s private Documents folder and optionally in the cloud.

The long answer is that you can store your documents anywhere your app has access to, but the only place that makes much sense is your app’s private Documents folder. Each iOS app has access to a cluster of private folders called its sandbox. The Documents folder is one of these and is reserved, by iOS, for your app’s documents. The contents of this folder are automatically backed up by iTunes. If you also want to exchange documents through iTunes, your documents must be stored in the Documents folder.

This is somewhat different from what you’re used to on most desktop computer systems, where apps will let you load and save documents to any location and your Documents folder is freely shared by all of your apps. In iOS, an app has access only to the files in its sandbox, and these directories are inaccessible to other apps or to the user—unless you deliberately expose the Documents folder to iTunes.

Note If you’re interested in what the other folders in the sandbox are and what they’re used for, read the section “About the iOS File System” in the File System Programming Guide, which you can find in Xcode’s Documentation and API Reference window.

For MyStuff, you’re going to store a single document in the Documents folder. You won’t, however, provide any user interface for this document. The document will be automatically opened when the app starts, and any changes made by the user will be automatically saved there. Even though you’ll be using the standard document classes and storing your data in the Documents folder, the entire process will be invisible to the user.

That’s not to say that you can’t, or shouldn’t, provide an interface that lets your users see what documents are in their Documents folder. A typical interface would display the document names, possibly a preview, and allow the user to open, rename, and delete them. You could do that by using a table view, using a collection view, or even using a page view controller. iOS 8 introduces a standardized interface that does just that. If you want a simple document picker interface, which also works with iCloud Drive, start with the UIDocumentPickerViewController class. If you still want to design your own interface, iOS also provides a new NSURLThumbnail resource—see NSURL.getResourceValue(_:,forKey:,error:) for retrieving URL resources—that makes displaying your document thumbnails simple.

But, as I said, you don’t need to expose the document structure to MyStuff users. You do need to create a custom subclass of UIDocument and define where and how your document gets stored, which sounds like the place to get started.

MyStuff on Documents

Pick up with the version of MyStuff at the end of Chapter 7, where you added an image for each item. Drag a new Swift file from the file template library into your project (or choose the New File command). Name the new file ThingsDocument and make it a subclass of UIDocumentwith the following code:

import UIKit

class ThingsDocument: UIDocument {
}

The first thing you’re going to add is a constant with the name of your one and only document. Add this outside the ThingsDocument class definition so it’s a global constant.

let ThingsDocumentName = "Things I Own.mystuff"

Now add a computed property to locate your app’s Document folder and return your single document file as a URL.

class var documentURL: NSURL {
let fileManager = NSFileManager.defaultManager()
if let documentsDirURL = fileManager.URLForDirectory( .DocumentDirectory,
inDomain: .UserDomainMask,
appropriateForURL: nil,
create: true,
error: nil) {

return documentsDirURL.URLByAppendingPathComponent(ThingsDocumentName)
}
assertionFailure("Unable to determine document storage location")
}

Your new documentURL property returns an NSURL object with the filesystem location of the one and only document used by your MyStuff app, named Things I own.mystuff.

The important method here is the URLForDirectory(_:,inDomain:,appropriateForURL:,create:,error:) function. This is one of a handful of functions used to locate key iOS directories, like the Documents directory in your app’s sandbox. The.DocumentDirectory constant tells which one—of the half-dozen or so designated directories—you’re interested in. To locate directories in your app’s sandbox, specify the .UserDomainMask. The create flag tells the file manager to create the directory if it doesn’t already exist. This was gratuitous because the Documents directory is created when your app is installed and should always exist, but it doesn’t hurt to say “yes” anyway.

Caution Do not “hard-code” paths to standard iOS directories, using constants like "~/Documents/". Use functions like URLsForDirectory(_:,inDomain:) to determine the path of well-known directories. The standard directory locations change from time to time, and you don’t want to make assumptions about their names or paths.

With the URL of your Documents folder, your code then appends the document’s name, creating a complete path to where your document is, or will be, stored.

Now write a function to open your document. MyStuff isn’t going to present a document interface. When it starts, it either creates an empty document or re-opens the existing document. Consolidate that logic into a single function, immediately after the documentURL property.

class func document(atURL url: NSURL = ThingsDocument.documentURL) -> ThingsDocument {
let fileManager = NSFileManager.defaultManager()
if let document = ThingsDocument(fileURL: ThingsDocument.documentURL) {
if fileManager.fileExistsAtPath(url.path!) {
document.openWithCompletionHandler(nil)
}
} else {
document.saveToURL(url, forSaveOperation: .ForCreating, completionHandler: nil)
}
return document
}
assertionFailure("Unable to create ThingsDocument for \(url)")
}

This function creates a new instance of your ThingsDocument object at the given (file) URL. If you don’t specify a file URL, it defaults to the documentURL property you just defined. It uses the file manager to determine whether a document at that location already exists (fileExistsAtPath(_:)). If it does, it calls the document’s openWithCompletionHandler(_:) function to open the document and read the data it contains. If it doesn’t exist, it calls the saveToURL(_:,forSaveOperation:,completionHandler:) function to save the document. Since the document object was just created, it’s empty, and saving it creates a new (empty) document. The opened document object is then returned to the sender.

Tip The name of MyStuff’s document is irrelevant because no one (except its developer) will ever see it. If, however, you do want your users to have access to the documents your app’s Documents folder, all you have to do is add the UIFileSharingEnabled key (with a value of YES) to your app’s info.plist. This flag tells iTunes to expose the documents stored in the Documents folder to the user. Through iTunes, the user can browse, download, upload, and delete documents in that folder. See the “App-Related Resources” chapter of theiOS App Programming Guide. Also check out Technical Q&A #1699 (QA1699). It describes how to selectively share some documents through iTunes, while keeping other documents hidden.

Supplying Your Document’s Data

In your subclass of UIDocument, you are required to override two functions: contentsForType(_:,error:) and loadFromContents(_:,ofType:,error:). These two methods translate your app’s data model objects into a form that UIDocument can save and later converts that saved data back into the data model objects your app needs.

This is also where implementing UIDocument gets interesting. The key is to understand what UIDocument is doing for you and what UIDocument expects from contentsForType(_:,error:) and loadFromContents(_:,ofType:,error:). There’s a strict division of responsibilities.

· UIDocument implements that actual storage and retrieval of your document’s data.

· contentsForType(_:,error:) and loadFromContents(_:,ofType:,error:) provide the translation between your data model objects and a serialized version of that same information.

UIDocument might be storing your document on a filesystem. It might be storing your document in the cloud. It might be transferring your document over a USB connection. Someday it might store your document on a wireless electronic wallet you carry around on a key fob. I don’t know, and you shouldn’t care. Let UIDocument worry about where and how your document’s data gets stored.

When UIDocument wants to save your document, it calls your contentsForType(_:,error:) function. Your implementation should convert your data model objects into data suitable for storage. UIDocument takes the returned data and stores it on the filesystem, in the cloud, or wherever.

When it’s time to read the document, UIDocument reverses the process. It first reacquires the data (from wherever it was saved) and passes that to loadFromContents(_:,ofType:,error:), which has the job of turning it back into the data model objects of your app.

The $64,000 question is “How do you convert your data model objects into bytes that UIDocument can store?” That is a fantastic question, and the answer will range from stunningly simple to treacherously complex. Broadly, you have four options.

· Serialize everything into a single NSData object

· Describe a multipart document using file wrapper objects

· Back your document with Core Data

· Implement your own storage solution

The first solution is the simplest and suitable for many document types. Using string encoding, property list serialization, or object archiving (which you’ll learn shortly), convert your data model object(s) into a single array of bytes. Your contentsForType(_:,error:) function then returns those bytes as an NSData object that UIDocument stores somewhere. Later, UIDocument retrieves that data and passes an NSData object to your loadFromContents(_:,ofType:,error:) function, which unarchives/deserializes/decodes it back into the original object(s). If this describes your app’s needs, then congratulations—you’re pretty much done with this part of your UIDocument implementation!

Your MyStuff app is a little more complicated. It’s cumbersome to convert all of the app’s data—descriptions and images—into a single stream of bytes. Images are big and time-consuming to encode. Not only will it take a long time to save the document, the entire document will have to be read into memory and converted back into image objects before the user can use the app. No one wants to wait ten seconds, and certainly not a whole minute, to open your app!

The solution MyStuff will employ is to archive the descriptions of the items (much like the first solution) into a single NSData object but store the images in individual files inside a package. A package is a directory containing multiple files that appears, and acts, like a single file to the user. All iOS and OS X apps are packages, for example.

Wrapping Up Your Data

You might be seeing the glimmer of a conundrum. Or, maybe you don’t. Don’t worry if you missed it, because it’s a really subtle problem. The concept behind contentsForType(_:,error:) is that it returns the raw data that represents your document—just the data. The code incontentsForType(_:,error:) can’t know how that data gets stored, nor does it do the storing. Creating a design that states “images will be stored in individual files” is a nonstarter because contentsForType(_:,error:) doesn’t deal with files. The returned data might end up being stored in something that doesn’t even resemble a file.

So, how does contentsForType(_:,error:) return an object that describes not one, but a collection of, individual data blobs,1 one of which contains the archived objects and others that contain individual image data? Well, it just so happens that iOS provides a tool for this very purpose. It’s called a file wrapper, and it brings us to the second method for providing document data.

A file wrapper (NSFileWrapper) object is an abstraction of the data stored in one or more files. There are three types of file wrappers: regular, directory, and link. Conceptually, these are equivalent to a single data file, a filesystem directory, and a filesystem symbolic link, respectively. File wrappers allow your app to describe a collection of named data blobs, organized within a hierarchy of named directories. If this sounds just like files and folders, it should. And when your UIDocument is stored in a file URL, that’s exactly what these file wrappers will become. But by maintaining this abstraction, UIDocument can just as easily transfer this data collection over a network or convert the wrappers into the records of a database.

Using Wrappers

Using file wrappers isn’t terribly difficult. A regular file wrapper represents an array of bytes, like NSData. A directory file wrapper (or just directory wrapper) contains any number of other file wrappers.

One significant difference between wrappers and files/folders is that a wrapper is not required to have a unique name. A wrapper has a preferred name and a key. Its key is the string that uniquely identifies the wrapper, just as a filename uniquely identifies a file. Its name or preferred name is the string it would like to be identified as. When you create a wrapper, you assign it a preferred name. If you then add it to a directory wrapper and its preferred name is unique, its key and preferred name will be the same. If, however, there is already one or more wrappers with the same name, the directory wrapper will generate a unique key for the just-added wrapper. In other words, it’s valid to add multiple wrappers with the same name to the same directory wrapper. Just be aware that adding a wrapper does not replace, or overwrite, an existing wrapper with the same name, as it would on a filesystem. And if you want to refer to it again, you’ll need to keep track of its key.

Your contentsForType(_:,error:) function will create a single directory wrapper (docWrrapper) that contains all of the other regular file wrappers. There will be one regular file wrapper with the archived version of your data model objects. Each item that has a picture will store its image as another file wrapper. You’ll modify MyWhatsit to store the image in the document when the user adds a picture and get the image from the document when it needs it again.

Incremental Document Updates

Organizing your document into wrappers confers a notable feature to your app: incremental document loading and updates. If your user has added 100 items to your MyStuff app, your document package (when saved to a filesystem) will consist of folding containing 101 files: one archive file and 100 image files. If the user replaces the picture of their astrolabe with a better one, only a single file wrapper needs to be updated. UIDocument understands this. When it’s time to save the document again, UIDocument will only rewrite that single file in the package. This makes for terribly fast, and efficient, updates to large documents. These are good qualities for your app.

Similarly, file wrapper data isn’t read until it’s requested. In other words, file wrappers are lazy. When you open a UIDocument constructed from file wrappers, the data for each individual wrapper stays where it is until your app wants it. For your images, that means your app doesn’t have to read all 100 images files when it starts. It can retrieve just the images it needs at that moment. Again, this means your app can get started quickly and does the minimum work required to display your interface.

Constructing Your Wrappers

Select the ThingsDocument.swift file. Start by adding two constants and two instance variables.

let thingsPreferredName = "things.data"
let imagePreferredName = "image.png"

var docWrapper = NSFileWrapper(directoryWithFileWrappers: [:])
var things = [MyWhatsit]()

The two constants define the preferred wrapper names for the archived MyWhatsit objects and any image added to the directory wrapper. The docWrapper instance variable is the single directory wrapper that will contain all of your other wrappers. For all intents and purposes,docWrapper is your document’s data. The things variable is the array of MyWhatsit objects that constitute your data model.

Note Later, you’ll replace the things array in MasterViewController with your new ThingsDocument. The document object will become the data model for your view controller.

Now add the crucial contentsForType(_:,error:) function.

override func contentsForType(typeName: String, image
error outError: NSErrorPointer) -> AnyObject? {
if let wrapper = docWrapper.fileWrappers[thingsPreferredName] as? NSFileWrapper {
docWrapper.removeFileWrapper(wrapper)
}
let thingsData = NSKeyedArchiver.archivedDataWithRootObject(things)
docWrapper.addRegularFileWithContents(thingsData, preferredFilename: thingsPreferredName)
return docWrapper
}

This function is called when UIDocument wants to create or save your document. The first step handles the second case, where you’re overwriting an existing wrapper; it checks to see whether the things.data subwrapper already exists and deletes it. Remember that adding anotherthings.data wrapper won’t replace the previous one.

The next step is to archive (serialize) all of the MyWhatsit objects into a portable NSData object. I’ll explain how that happens in the next section. The resulting data object is then passed to the addRegularFileWithContents(_:,preferredFilename:) function. This is a convenience method that creates a new regular file wrapper, containing the bytes in thingsData, and adds it to the directory wrapper with the preferred name. This method saves you from explicitly coding those steps.

Finally, you return the directory wrapper, containing all of the data in your document, to UIDocument. Now you might be asking, “But what about all of the image data? Where does that get created?” That’s a really good question. Image data is represented by other regular file wrappers in the same directory wrapper. When the document is first created, there are no images, so the directory wrapper only contains things.data. As the user adds pictures to the data model, each image will add a new wrapper to docWrapper. When your document is saved again, the file wrappers containing the images are already in docWrapper! Each regular file wrapper knows if it has been altered or updated, and UIDocument is smart enough to figure out which files need to be written and which ones are already current.

Interpreting Your Wrappers

The reverse of the previous process occurs when your document is opened. UIDocument obtains that data saved in the document and then calls your loadFromContents(_:,ofType:,error:) function. This function’s job is to turn the document data back into your data model. Add this function immediately after your contentsForType(_:,error:).

override func loadFromContents(contents: AnyObject,image
ofType typeName: String,image
error outError: NSErrorPointer) -> Bool {
if let contentWrapper = contents as? NSFileWrapper {
if let thingsWrapper = contentWrapper.fileWrappers[thingsPreferredName]image
as? NSFileWrapper {
if let data = thingsWrapper.regularFileContents {
things = NSKeyedUnarchiver.unarchiveObjectWithData(data) as [MyWhatsit]
for thing in things {
thing.imageStorage = self
}
docWrapper = contentWrapper
return true
}
}
}
return false
}

The contents parameter is the object that encapsulates your document’s data. It’s always going to be the same (class of) object you returned from contentsForType(_:,error:). If you adopted the first method and returned a single NSData object, the contents parameter will contain an NSData object, with the same data. Since MyStuff elected to use the file wrapper technique, contents is an equivalent directory wrapper object to the one you returned earlier.

The first step is to save contents in docWrapper; you’ll need it, both to read image wrappers and to later save the document again. The rest of the method finds the things.data wrapper that contains the archived MyWhatsit object array. It immediately retrieves the data stored in that wrapper and unarchives it, re-creating your data model objects.

The loadFromContents(_:,ofType:,error:) function must return true if it was successful or false if there were problems interpreting the document. If the wrapper contained a things.data wrapper and the data in that wrapper was successfully converted back into an array of MyWhatsit objects, the method assumes the document is valid and returns true.

This, almost, concludes the work needed to save, and later open, your new document. There’s one glaring hole: the array of MyWhatsit objects can’t be archived! Let’s fix that now.

OTHER STORAGE ALTERNATIVES

The last two document storage solutions available to you are Core Data and do it yourself (DIY). DIY is one I rarely find appealing. It should be your last resort, because you’ll be forced to deal with all of the tasks, both mundane and exceptional, that UIDocument normally handles for you. My advice is work very hard to make one of the first three solutions work. If that fails, you can perform your own document storage handling. Consult the “Advanced Overrides” section of UIDocument’s documentation.

One of the most interesting document solutions is Core Data. iOS includes a fast and efficient relational database engine (SQLite) with language-level support. Core Data is far beyond the scope of this book, but it’s an incredibly powerful tool if your app’s data fits better into a database than a text file. (It’s a shame I don’t have enough pages because MyStuff would have made a perfect Core Data app.)

One of the huge advantages of using Core Data is that document management is essentially done for you. You don’t have to do much beyond using the UIManagedDocument class (a subclass of UIDocument). Many of the features in this chapter that you will write code to support—incremental document updating, lazy document loading, archiving and unarchiving of your data model objects, background document loading and saving, cloud synchronization, and so on—are all provided “for free” by UIManagedDocument.

The prerequisite, of course, is that you must first base your app on Core Data. Your data model objects must be NSManagedObjects, you must design a schema for your database, and you have to understand the ins and outs of object-oriented database (OODB) technology. But beyond that (!), it’s child’s play.

Archiving Objects

In Chapter 18 you learned all about serialization. Serialization turns a graph of property list objects into a stream of bytes (either in XML or in binary format) that can be stored in files, exchanged with other processes, transmitted to other computer systems, and so on. On the receiving end, those bytes are turned back into an equivalent set of property list objects, ready to be used.

Archiving is serialization’s big sister. Archiving serializes (the computer science term) a graph of objects that all adopt the NSCoding protocol. This is a much larger set of objects than the property-list objects.2 More importantly, you can adopt the NSCoding protocol in classes you develop. Your custom objects can then be archived right along with other objects. This is exactly what needs to happen to your MyWhatsit class.

Adopting NSCoding

The first step to archiving a graph of objects is to make sure that every object adopts the NSCoding protocol. If one doesn’t, you either need to eliminate it from the graph or change it so it does. In MyWhatsit.swift, change the class declaration so it adopts NSCoding (new code in bold).

class MyWhatsit: NSObject, NSCoding {

Note You adopt NSCoding by first making your class a subclass of NSObject, the Objective-C base class. You do this for the same reason you did in Chapter 8, making your class compatible with Key-Value Observing; NSObject defines key methods that NSCoding depends on. Note that the @objc Swift keyword would accomplish the same.

The NSCoding protocol requires a class to implement an initializer, init(coder:), and an instance function, encodeWithCoder(_:). The initializer creates a new object from data that was previously archived. The function creates the archive data from the existing object. Both of these processes work through an NSCoder object. The NSCoder object does the work of serializing (encoding), and later deserializing (decoding), your object’s properties.

The coder identifies each property value of your object using a key. Define those keys now by adding these constants to your MyWhatsit class.

let nameKey = "name"
let locationKey = "location"

Now you can write the initializer function.

required init(coder decoder: NSCoder) {
name = decoder.decodeObjectForKey(nameKey) as String
location = decoder.decodeObjectForKey(locationKey) as String
}

init(coder:) initializes all of the new object’s properties from the values stored in the coder object. In this case, both of the values are string objects. Besides objects, coder objects can directly encode integer, floating-point, Boolean, and other primitive types. UIKit adds extensions toNSCoder to encode point, rectangle, size, affine transforms, and other commonly encoded data structures. Now write your initializer’s mirror image.

func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(name, forKey: nameKey)
coder.encodeObject(location, forKey: locationKey)
}

Translation in the other direction is provided by your encodeWithCoder(_:) function. This function preserves the current values of its persistent properties in the coder object. Your MyWhatsit objects are now ready to participate in the archiving process.

SUBCLASSING AN <NSCODING> CLASS

When you subclass a class that already adopts NSCoding, you do things a little differently. Your init(coder:) function will look like this:

required init(coder decoder: NSCoder) {
super.init(coder: decoder)
// subclass decoding goes here
}

And your encodeWithCoder(_:) function should look like this:

override func encodeWithCoder(coder: NSCoder) {
super.encodeWithCoder(coder)
// subclass encoding goes here
}

Your superclass already encodes and decodes its properties. Your subclass must allow the superclass to do that and then encode and decode any additional properties defined in the subclass.

Archiving and Unarchiving Objects

Once your class has adopted NSCoding, it’s ready to be archived. When you want to flatten your object into bytes, use code like this:

let data = NSKeyedArchiver.archivedDataWithRootObject(myObject)

The NSKeyedArchiver class is the archiving engine. It creates an NSCoder object and then proceeds to call the root object’s (things’) encodeWithCoder(_:) function. That object is responsible for preserving its content in the coder object. Most likely, it will call itsencodeObject(_:,forKey:) function for objects it refers to. Those objects then receive an encodeWithCoder(_:) call, and the process repeats until all of the objects have been encoded. The only limitation is that every object involved must adopt NSCoding.

When you want your objects back again, you use the NSKeyedUnarchiver class, like this:

myObject = NSKeyedUnarchiver.unarchiveObjectWithData(data)

During the encoding process, the coder recorded the class of each object. The decoder then uses that information to invoke the object’s init(coder:) initializer. The resulting object is the same class and has the same property values as the originally encoded object.

Note The predecessor to keyed archiving was sequential archiving. You may occasionally see references to sequential archiving, but it is not used in iOS.

The Archiving Serialization Smackdown

Now that you’ve added both serialization (property lists) and archiving (NSCoding objects) to your repertoire, I’d like to take a moment to compare and contrast the two. Table 19-1 summarizes their major features.

Table 19-1. Serialization vs. Archiving

Feature

Serialization

Archiving

Object Graph

Property list objects only

Objects that adopt NSCoding

XML

Yes

No

Portability

Cocoa or Cocoa Touch apps, or any system that can parse the XML version

Only another process that includes all of the original classes

Editors

Yes

No

Property lists are much more limited in what you can store in them but make up for that in the number of ways you can store, share, and edit them. Use property lists when your values need to be understood by other processes, particularly processes that don’t include your custom classes. An example is the settings bundle you created in Chapter 18. The Settings app will never include any of your custom Objective-C classes, yet you were able to define, exchange, and incorporate those settings into your app using property lists. Property lists are the “universal” language of values.

Archiving, by contrast, can encode a vast number of classes, and you can add your own classes to that roster by adopting the NSCoding protocol. Everything you create in Interface Builder is encoded using keyed archiving. When you load an Interface Builder file in your application,NSKeyedUnarchiver is busy translating that file back into the objects you defined. Archiving is extremely flexible and has long reach, which is why it’s the technology of choice for storing your data model objects in a document.

Why don’t we use archiving for everything? When unarchiving, every class recorded in the archive must exist. So, forget about trying to read your MyStuff document using another app or program that doesn’t include your MyWhatsit class—you can’t do it. Archives are, for the most part, opaque. There are no general-purpose editors for archives like there are for property lists, and there is no facility for turning archive data into XML documents.

Serialization, Meet Archiving

Now that you have a feel for the benefits and limitations of archiving and serialization, I’m going to show you a really handy trick for combining the two. (You may have already figured this out, but you could at least pretend to be surprised.) NSData is a property list object. The result of archiving a graph of NSCoding objects is an NSData object. Do you see where this is going?

By first archiving your objects into an NSData object, you can store non-property-list objects in a property list, like user defaults! Your code would look like this:

let userDefaults = NSUserDefaults.standardUserDefaults()
let data = NSKeyedArchiver.archivedDataWithRootObject(dataModel)
userDefaults.setObject(data, forKey: "data_model")

What you’ve done is archive your data model objects into an NSData object, which can be stored in a property list. To retrieve them again, reverse the process.

let modelData = userDefaults.objectForKey("data_model") as NSData
dataModel = NSKeyedUnarchiver.unarchiveObjectWithData(modelData) as DataClass

The disadvantages of this technique are the same ones that apply to archiving in general. The process retrieving the objects must to be able to unarchive them. Also, any editors or other programs that examine your property values will just see a blob of data. Contrast this to the technique you used in Pigeon to convert the MKAnnotation object into a dictionary. Those property list values (the location’s name, longitude, and latitude) are easily interpreted and could even be edited by another program.

Caution Don’t go crazy with this technique. Services like NSUserDefaults and NSUbiquitousKeyValueStore are designed to store small morsels of information. Don’t abuse them by storing multimegabyte-sized NSData objects that you’ve created using the archiver.

I think that’s enough about archiving and property lists. It’s time to get back to the business of getting MyStuff documentified.

Document, Meet Your Data Model

Where were we? Oh, that’s right, you created a UIDocument class and wrote all of the code needed to translate your data model objects into document data and back again. The next step is to make your ThingsDocument object the data model for MasterViewController.

You’re doing this because your document needs to be aware of any changes to your data model, which I’ll explain in the “Tracking Changes” section that follows. Right now, your view controller is the object manipulating your data model (the things array). This isn’t good MVC design; your controller is doing some of the data model’s job. But it wasn’t bad enough to warrant creating another data model class just to encapsulate changes to the things array. With documents in the mix, we just crossed that line, so it’s time to refactor. As I’ve said at the end of Chapter 8, it’s OK to venture off the MVC path a little when it keeps your code simple. Just know where you did and be prepared to get back on track when you find yourself in the weeds.

Note In a “big” app, you’d probably create a custom data model class that was separate from your UIDocument class. Both the document and view controller would then observe changes to the data model object. In MVC-speak, you’d have a data model and a data model controller (the document object). Both the document and the view controller would connect to the same data model object. For MyStuff, I’m having you combine the data model and document into a single class for the same reason the data model and view controller were entangled before. It simplifies the design and reduces the amount of code you have to write.

Your current MasterViewController is using an array as its data model object. The array object provides a number of methods that the view controller is using to manage it, specifically counting, adding, and removing objects in the array. UIDocument doesn’t have any of these methods—because it’s not a data model. Turn it into a data model by replicating the functions the view controller needs. Select ThingsDocument.swift and add the following functions:

var whatsitCount: Int {
return things.count
}

func whatsitAtIndex(index: Int) -> MyWhatsit {
return things[index]
}

func indexOfWhatsit(thing: MyWhatsit) -> Int? {
for (index,value) in enumerate(things) {
if value === thing {
return index
}
}
return nil
}

func removeWhatsitAtIndex(index: Int) {
things.removeAtIndex(index)
}

func anotherWhatsit() -> (object: MyWhatsit, index: Int) {
let newThing = MyWhatsit(name: "My Item \(whatsitCount+1)")
things.append(newThing)
return (newThing, things.count-1)
}

The purpose of these methods should be obvious. The view controller will now call these functions to count the number of items, get the item at a specific index, discover the index of an existing item, remove an item, or create a new item. The next step is to change your view controller to use this interface. Select your MasterViewController.swift file. Replace the var things: [MyWhatsit]... declaration with the following:

var document = ThingsDocument.document()

Your document object is now your data model. You also removed the code that created the fake items for testing. Now that your app is using documents, it will save the items as you create them. Now you need to go through your view controller code and replace every reference to the oldthings array with equivalent code for your document.

Tip Your file is now awash with compiler errors. Isn’t that great? I use this technique all the time. When I need to redefine or repurpose a property value, I deliberately change the name of the property/variable—if only temporarily. Xcode will immediately flag all references to the old name as an error. This becomes my road map to where I need to make my changes. If I liked the original property name, I’ll restore it once everything is working.

The rest of the work is mostly replacing code that used things with code that will use document. Find the insertNewObject(_:) function and change it so it reads as follows (modified code in bold):

func insertNewObject(sender: AnyObject) {
let fresh = document.anotherWhatsit()
let indexPath = NSIndexPath(forRow: fresh.index, inSection: 0)
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}

The document object now takes care of creating a new MyWhatsit object—you’ll understand why when you work on the code for MyWhatsit images. The code also gets the index of the new object from the document, rather than assuming that it was inserted at the beginning or end of the array. This is a smart change because the anotherWhatsit() function actually might change its mind someday. And if you ever altered that, this code would still work.

The other “big” change is in the whatsitDidChange(_:) function. Alter it as shown (modified code in bold):

func whatsitDidChange(notification: NSNotification) {
if let changedThing = notification.object as? MyWhatsit {
if let index = document.indexOfWhatsit(changedThing) {
let path = NSIndexPath(forItem: index, inSection: 0)
tableView.reloadRowsAtIndexPaths([path], withRowAnimation: .None)
}
}
}

The loop that looked for the object in the array is replaced with a function call that does the same. The rest of the changes are so mundane that I’ve summarized them here. (Hint: just follow the trail of compiler errors and replace the things statements with equivalent documentstatements.)

· In tableView(_:,numberOfRowsInSection:) the statement

things.count becomes document.whatsitCount.

· In tableView(_:,cellForRowAtIndexPath:) and prepareForSeque(_:) the

things[indexPath.row] expression becomes document.whatsitAtIndex(indexPath.row).

· In tableView(_:,commitEditingStyle:,forRowAtIndexPath:) the

things.removeAtIndex(indexPath.row) expression becomes document.removeWhatsitAtIndex(indexPath.row).

Your ThingsDocument object is now your app’s data model. This is an important step. It’s not important that you combined the document and data model into a single object, but it is important that you’ve encapsulated all of the changes to the data model—counting, getting, removing, and creating items—behind your own methods, rather than simply using array methods. You’ll see why shortly.

You might think that you’ve written enough code that your app would be able to store its MyWhatsit objects (at least the name and location bits) in your document and retrieve them again. But there are still a few small pieces of the document puzzle missing.

Tracking Changes

One thing you haven’t written is any code to save your document. You’ve written code to convert your data model objects into something that can be saved, but you’ve never asked the UIDocument object to save itself.

And you won’t.

At least, that’s not the ideal technique. UIDocument embraces the autosave document model, where the user’s document is periodically saved to persistent storage while they work and again automatically before your app quits. This is the preferred document-saving model for iOS apps.

For autosaving to work, your code must notify the document that changes have been made. UIDocument then schedules and performs the saving of the new data in the background. There are two ways to communicate changes to your document: call the updateChangeCount(_:_function or use the document’s NSUndoManager object. As you register changes with the NSUndoManager, it will automatically notify its document object of changes.

Note The alternative to using an undo manager and autosaving is to explicitly save the document by calling saveToURL(_:,forSaveOperation:,completionHandler:) (or one of the closely related functions). This would imply an interface that works more like legacy desktop applications, where the user must deliberately save their document.

You’re not going to embrace NSUndoManager for this app—although it’s a great feature to consider and not at all difficult to use. Consequently, you’ll need to call your document object’s updateChangeCount(_:) function whenever something changes. UIDocument will take it from there.

So, when does your data model change? One obvious place is whenever items are added or removed. Select the ThingsDocument.swift file. Locate the removeWhatistAtIndex(_:) and anotherWhatsit() functions. At the end of removeWhatistAtIndex(_:) and again just before the return statement in anotherWhatsit(), add the following statement:

updateChangeCount(.Done)

This message tells the document object that its content was changed, and those changes are .Done. There are other kinds of changes (changes because of an undo or redo action, for example), but unless you’ve created your own undo manager, this is the only constant you need to pass.

The other place that the document changes is when the user edits an individual item. You already solved that problem way back in Chapter 4! Whenever a MyWhatsit object is edited, your object posts a MyWhatsitDidChange notification. All your document needs to do is observe that notification.

Still in your ThingsDocument.swift file, add the following initializer and deinitializer:

override init?(fileURL url: NSURL) {
super.init(fileURL: url)
let center = NSNotificationCenter.defaultCenter()
center.addObserver( self,
selector: "thingsDidChange:",
name: WhatsitDidChangeNotification,
object: nil)
}

deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}

The initializer registers your document to receive WhatsitDidChangeNotification notifications, and the deinitializer unregisters before your document object is destroyed.

Finally, add the new notification handler method.

func thingsDidChange(notification: NSNotification) {
if indexOfWhatsit(notification.object as MyWhatsit) != nil {
updateChangeCount(.Done)
}
}

Its only purpose is to notify the document that a MyWhatsit object in this document has changed, and that’s what it does.

Testing Your Document

Surely, you’ve written enough code by now to see your document in action. Run your app, either in the simulator or in a provisioned device. It contains nothing when first launched, as shown on the left in Figure 19-1. Enter the details for a couple of items.

image

Figure 19-1. Testing document storage

Now either wait about 20 seconds or press the home button to push the app into the background state. When you created a new item, the document was notified of the change. The autosave feature of UIDocument periodically saves the document when the user isn’t doing anything else and will immediately save it when your app is moved to the background state.

With your data safely saved in the document, stop the app and run it again from Xcode. You should be rewarded for all of your hard work with the list of items you entered earlier.

What you see, however, is an empty screen, as shown on the right in Figure 19-1.

So, what went wrong? Maybe your document isn’t being opened when your app starts? Maybe it didn’t get saved in the first place? What you do know is that you’ve got a bug; it’s time to turn to the debugger.

Setting Breakpoints

Switch back to Xcode and set a breakpoint in your contentsForType(_:,error:) by clicking in the gutter to the left of the code, as shown in Figure 19-2. A breakpoint appears as a blue tab.

image

Figure 19-2. Setting a breakpoint in contentsForType(_:,error:)

Uninstall your My Stuff app on your device or simulator. (Tap and hold the My Stuff app icon in the springboard until it starts to shake, tap the delete [x] button, agree to delete the app, and press the home button again.) This deletes your app and any data, including any documents, stored on the device. Run the app again. Xcode will reinstall the app, and it will run with a fresh start.

Almost immediately, Xcode stops at the breakpoint in the contentsForType(_:,error:) function, as shown in Figure 19-2. If you look at the stack trace on the left, you can see that the contentsForType(_:,error:) function was called from the document() function, which was called from MasterViewController.init(). This tells you that contentsForType(_:,error:) is being sent to create the initial, empty document when no document exists. Remember that when the document doesn’t exist, the first thing document() does is create one by saving the empty document object.

Stepping Through Code and Examining Variables

So, you know the empty document is getting saved. What about the next step? Click the continue button in the debugger ribbon (also shown in Figure 19-2). The lets your app resume normal execution. Add some new objects and either wait a bit or press the home button to push your app into the background. Again, Xcode will stop at the breakpoint in contentsForType(_:,error:). This tells you your document is being autosaved when you make changes to it. So far, so good.

If your document is getting written correctly, maybe it’s not getting loaded correctly. Set another breakpoint in the loadFromContent(_:,ofType:,error:) and run your app again, as shown in Figure 19-3. Once Xcode stops in this function, click the Step Over button (right next to the Continue Execution button) to execute one statement at a time. Click it repeatedly until the statement that sets the things array has executed, as shown in Figure 19-3.

image

Figure 19-3. Stepping through contentsForType(_:,error:)

Tip Step Over executes a complete statement in your source code and stops when it finishes. Step Into executes one statement; if it’s a function call, it will move into that function and stop again. Step Out allows the remainder of a function to execute, stopping again when it returns to its caller.

This is the function called to load the contents of your document. It unarchives the data and populates the things array. Look in the debugger area at the bottom of the workspace window, and you’ll see all of the active variables in this function. One of those is self, the document object being acted upon. Expand it to examine its property values. In it, you’ll find a line that says (something like) the following:

things = ([(MyStuff.MyWhatsit)]) 2 values

That statement says that the things property of your document object consists of an array of MyWhatsit objects, and it currently contains two objects. This is great! It means your document was successfully read, and the previously serialized data has been reconstructed as twoMyWhatsit objects.

So, why aren’t they showing up in your table view? Let’s find out. Locate the whatsitCount property and set a breakpoint on its single return statement. (Leave those other breakpoints set.) Run your app again. One of the first things a table view does is get the number of rows in the table from its data source delegate. That function, in turn, reads your whatsitCount property. Sure enough, as soon as you run your app, Xcode stops in your whatsitCount property getter, as shown in Figure 19-4.

image

Figure 19-4. Examining the whatsitCount property

Again, expand the self variable in the debugging pane and look at the things property. This time it’s empty. Have you figured out the problem yet? Click the Continue Execution button and let your app run. The next thing that happens is you hit the breakpoint inloadFromContent(_:,ofType:,error:). Have you figured out the problem yet?

Tip By the way, this is called the “divide and conquer” debugging technique. Decide what your code should be doing, set a breakpoint somewhere in the middle of that process, and see whether that step is happening correctly. If not, the problem either is right there or earlier in your code. If it is happening correctly, the problem is after that point. Choose another breakpoint and repeat until you’ve found the bug.

Here’s the problem. UIDocument’s openWithCompletionHandler(_:) function (called from document()) is asynchronous. It starts the process of retrieving your document’s data in the background and returns immediately. Your app’s code proceeds, displaying the table view, with a still empty data model.

Some time later, the data for the document finishes loading and is passed to loadFromContents(_:,ofType:,error:) to be converted into a data model. That’s successful, but the table view doesn’t know that and continues to display—what it thinks is—an empty list.

What your document needs to do is notify your view controller when the data model has been updated, so the table view can refresh itself. You could accomplish this using a notification, but I think the most sensible solution is to use a delegate function. As a bonus, you’ll get practice creating your own protocol.

Tip Remove a breakpoint by dragging it out of the gutter. Relocate a breaking by dragging it to a new location. Disable or enable a breakpoint by clicking it.

Creating a ThingsDocument Delegate

Define a new delegate protocol. You could add a new Swift file to the project just for this protocol, but since it goes hand in hand with the ThingsDocument class, I recommend adding it right to the ThingsDocument.swift file.

protocol ThingsDocumentDelegate {
func gotThings(document: ThingsDocument)
}

This defines a protocol with one function (gotThings(_:)), to be called whenever your document object loads new things from the document. To the ThingsDocument class, add a new delegate property as follows (new code in bold):

class ThingsDocument: UIDocument, ImageStorage {
var delegate: ThingsDocumentDelegate?

Find the document(atURL:) function. Change the statement that opens the document to this (modified code in bold):

document.openWithCompletionHandler() { (success) in
if success {
document.delegate?.gotThings(document)
}
}

The modified code now performs an action after the document is finished loading, which includes the unarchiving of the data model objects. Now it calls its delegate function gotThings(_:), so the delegate (your view controller) knows that the data model has changed.

Switch to the MasterViewController.swift file and make your view controller a document delegate (new code in bold).

class MasterViewController: UITableViewController, ThingsDocumentDelegate {

Find the awakeFromNib() function and add a statement at the end to make the view controller the document’s delegate object (new code in bold).

document.delegate = self

Finally, write the protocol function gotThings(_:), as follows:

func gotThings(_: ThingsDocument) {
tableView.reloadData()
}

Run your app again, as shown in Figure 19-5, and voilà! The data in your document appears in the table view.

image

Figure 19-5. Working document

Make changes or add new items. Press the home button to give UIDocument a chance to save the document, stop the app, restart it, and your changes persist. The only content MyStuff doesn’t save is any images you add. That’s because images aren’t part of the archived object data. You’re going to add image data directly to the document’s directory wrapper, so attack that problem next.

Tip The Debug image Deactivate Breakpoints command will disable all breakpoints in your project, allowing you to run and test your app without interruption.

Storing Image Files

In the preceding sections, you learned all the basics of serializing your data model objects, storing them in a document file, and retrieving them again. Image data storage takes a different route than the other properties in your MyWhatsit objects. Here is how it’s going to work:

· When a new, or updated, image (UIImage) object is added to a MyWhatsit object, the image is converted into the Portable Network Graphics (PNG) data format and stored in the document as a file wrapper. The MyWhatsit object remembers the key of the file wrapper.

· When the document is saved, UIDocument includes the data from all the file wrappers in the document wrapper. The image file wrapper keys are archived by the MyWhatsit objects.

· When the document is opened again, the file wrapper objects for the image data are restored.

· When client code requests the image property of a MyWhatsit object, MyWhatsit uses its saved key to locate and load the data in the file wrapper, eventually converting it back into the original UIImage object.

The key to this design (no pun intended) is the relationship between the MyWhatsit objects and the document object. A MyWhatsit object will use the document object to store and later retrieve the data for an individual image. From a software design standpoint, however, you want to keep the code that actually stores and retrieves the image data out of the MyWhatsit object. The single responsibility principle encourages the MyWhatsit object to do what it does (represent the values in your data model) and the document object to do what it does (manage the storage and conversion of document data) without polluting one class with the responsibilities of the other.

The solution is to create an abstraction layer, or abstract service, in the ThingsDocument class to store and retrieve images. MyWhatsit will still instigate image management, but the mechanics of how those images get turned into file wrappers stays inside ThingsDocument. Let’s get started.

Add a second protocol to the ThingsDocument.swift file, as follows:

protocol ImageStorage {
func keyForImage(newImage: UIImage?, existingKey: String?) -> String?
func imageForKey(key: String?) -> UIImage?
}

This protocol defines a service that will store an image and retrieve an image. The first function will store, replace, or remove an image from storage, returning a key that can later be used to retrieve it. The second function performs that retrieval. Your ThingsDocument class will provide this service, so add it to its repertoire (modified code in bold).

class ThingsDocument: UIDocument, ImageStorage {

Now modify MyWhatsit to use these methods to save and restore its image property. Select the MyWhatsit.swift file and add two new properties, as follows, one for the image storage provider and a second to remember the image’s key in the store:

var imageStorage: ImageStorage?
var imageKey: String?

Now rewrite the image property. You’re going to change it from a simple stored property to a computed property that lazily obtains the image from imageStore when requested and encodes the image in imageStore when set. Rewrite var image as follows (new code in bold):

var image: UIImage? {
get {
if image_private == nil {
image_private = imageStorage?.imageForKey(imageKey)
}
return image_private
}
set {
image_private = newValue
imageKey = imageStorage?.keyForImage(newValue, existingKey: imageKey)
postDidChangeNotification()
}
}
private var image_private: UIImage?

You’ve refactored the image property to store and retrieve its image from an external source, the details of which are known only to imageStorage. No code that uses the image property changes. As far as the rest of your app is concerned, your MyWhatsit object still has an imageproperty that can be got or set.

To retrieve the image the next time the document is loaded, your new MyWhatsit object must remember the key returned from keyForImage(_:,existingKey:). Modify your NSCoding functions, as follows, so the imageKey property is also serialized (new code in bold):

let nameKey = "name"
let locationKey = "location"
let imageKeyKey = "image.key"

required init(coder decoder: NSCoder) {
name = decoder.decodeObjectForKey(nameKey) as String
location = decoder.decodeObjectForKey(locationKey) as String
imageKey = decoder.decodeObjectForKey(imageKeyKey) as? String
}

func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(name, forKey: nameKey)
coder.encodeObject(location, forKey: locationKey)
coder.encodeObject(imageKey, forKey: imageKeyKey)
}

Note Your NSCoding methods do not encode or decode either the image or document property of the object. When the object is unarchived, these property values will be nil. This makes them transient properties. Properties preserved by archiving are called persistentproperties.

That concludes most of the changes to the MyWhatsit class. Now you have to actually provide the image storage services you promised in the protocol. Select the ThingsDocument.swift file. Start by writing the image storage function, as follows:

func keyForImage(newImage: UIImage?, existingKey: String?) -> String? {
if let key = existingKey {
if let wrapper = docWrapper.fileWrappers[key] as? NSFileWrapper {
docWrapper.removeFileWrapper(wrapper)
}
}
var newKey: String? = nil
if let image = newImage {
let imageData = UIImagePNGRepresentation(image)
newKey = docWrapper.addRegularFileWithContents( imageData,image
preferredFilename: imagePreferredName)
}
updateChangeCount(.Done)
return newKey
}

The newImage parameter is either the image to store or nil if an image should not be stored. The image is stored by converting it into the PNG file format and storing that data in a regular file wrapper.

The existingKey parameter is the key of the previously stored image or nil if there wasn’t one. If supplied, the key is used to first discard the previously stored image file.

The function returns the key used to retrieve the stored image (if any). Using different combinations of values and nil, the function can be used to store a new image (image and no key), replace an image (image and key), or remove (no image and key) an image in the document.

That takes care of storing a new image in the document and replacing an existing image with a new one. Now add the code to retrieve images from the document, as follows:

func imageForKey(imageKey: String?) -> UIImage? {
if let key = imageKey {
if let wrapper = docWrapper.fileWrappers[key] as? NSFileWrapper {
return UIImage(data: wrapper.regularFileContents!)
}
}
return nil
}

This function uses imageKey to find the file wrapper in the document, calls the wrapper’s regularFileContents() function to retrieve its data, and uses that to reconstruct the original UIImage object, which is returned to the caller.

Note The data that a regular file wrapper represents isn’t read into memory until you call its regularFileContents() function. File wrappers are just lightweight placeholders for the data in persistent storage, until you request that data.

Sneakily, there’s one more place where an image is removed from the document—when the user deletes a MyWhatsit object. Locate the removeWhatsitAtIndex(_:) function. Add code to the beginning of the method to remove the image file wrapper for that item, before removing that item.

But what should this code look like? Just as you don’t want your data model classes having intimate knowledge about how images get stored in the document, your document object shouldn’t have intimate knowledge about how your data model classes are using ImageStorage. So, let’s keep that knowledge located in the MyWhatsit class. Add the following code to your removeWhatsitAtIndex(_:) function (new code in bold):

func removeWhatsitAtIndex(index: Int) {
let thing = whatsitAtIndex(index)
thing.willRemoveFromStorage()
thing.imageStorage = nil
things.removeAtIndex(index)
updateChangeCount(.Done)
}

Instead of removing the image file wrapper for it, you simply let the MyWhatsit object know that you’re about to remove it from a document. It will then take care of whatever it needs to do to remove itself. Finally, you disconnect it from your document (image) store so it will behave like a stand-alone MyWhatsit object again.

Oh, you better add that function to your MyWhatsit.swift file, as follows:

func willRemoveFromStorage() {
imageStorage?.keyForImage(nil, existingKey: imageKey)
imageKey = nil
}

All of the mechanics for saving, retrieving, and deleting images from the document are in place. Sadly, none of it will work. The MyWhatsit must be connected to the working ThingsDocument object through its imageStore property for any of this new code to function. At this point, no one is setting that property.

So, where should the imageStore property be set, and what object should be responsible for setting it? The answer is the ThingsDocument object. It should take responsibility for maintaining the connection between itself and its data model objects.

As it turns out, this is an incredibly easy problem to solve because there are only two locations where MyWhatsit objects are created: when the document is unarchived and when the user creates a new item. Start with the anotherWhatsit() function and add a statement to set the new object’s imageStore property (new code in bold), as follows:

func anotherWhatsit() -> (object: MyWhatsit, index: Int) {
let newThing = MyWhatsit(name: "My Item \(whatsitCount+1)")
newThing.imageStorage = self

Note Functions such as anotherWhatsit() are called factory methods. A factory method creates new, correctly configured objects for the client. The objects might be different classes or need to be initialized in a special way—like being added to a collection and having theirimageStore property set—before being returned. Write factory methods to create objects that need to be created in a way that the sender shouldn’t be responsible for.

Locate the loadFromContents(_:,ofType:,error:) function. Immediately after the things array is unarchived, add a loop to assign this document as the image store for all of them (new code in bold).

things = NSKeyedUnarchiver.unarchiveObjectWithData(data) as [MyWhatsit]
for thing in things {
thing.imageStorage = self
}
docWrapper = contentWrapper
return true

Your document implementation is finally finished! Give it a spin by running MyStuff. Add some items, attach some pictures, and quit the app, as shown in Figure 19-6. Stop the app in Xcode and start it again. All of the items, along with their pictures, are preserved in the document.

image

Figure 19-6. Testing image storage

Note In the rush to add image storage to your MyWhatsit object, I wanted to make sure you didn’t miss a remarkable fact: you did not change the interface to your data model. None of the code that uses the MyWhatsit object, like the code in DetailViewController, required any modifications. That’s because the meaning and use of the image property never changed. The only thing that changed was how that data gets stored. This is encapsulation and refactoring at work.

If you’re running MyStuff on a provisioned device, you can see your app’s document file(s) in the devices window (Window image Devices) in Xcode. Open the Devices window and select your device, and the applications installed on your device are listed, as shown in Figure 19-7. Select the MyStuff app and choose the Show Container command, also shown in Figure 19-7.

image

Figure 19-7. Showing MyStuff’s sandbox container

In the sheet that appears (see Figure 19-8), you can browse the files that make up your app’s sandbox. You can clearly see your Things I Own.mystuff document package inside your app’s Documents folder. The funny filenames (such as 1__#$!@%!#__image.png) are howUIDocument handles two or more file wrappers with the same preferred name. It gives the files crazy names so they can all be stored in the same directory.

image

Figure 19-8. The files in MyStuff’s sandbox

If you need to get at these files, use the Download Container command instead (see Figure 19-7). Xcode will copy the files from your iOS device to your hard drive, where you can play with them.

Odds and Ends

What you’ve accomplished in MyStuff is a lot, but it really represents the bare minimum of support required for document integration. There are lots of features and issues that I skipped over. Let’s review a couple of those now.

iCloud Storage

You can store your documents in the cloud, much like your stored property list values in the cloud in Chapter 18. Documents, naturally, are a little more complicated.

Apple’s guidelines suggest that you provide a setting that allows the user to place all of their documents in the cloud or none of their documents in the cloud. A piecemeal approach is not recommended. When the user changes their mind, your app is responsible for moving (copying) the locally stored documents into the cloud or in the other direction. This isn’t a trivial task. It involves multitasking, which I don’t get to until Chapter 24.

Once in the cloud, you open, modify, and save documents much the way you do from your local sandbox. All of the code you wrote for contentsForType(_:,error:) and loadFromContents(_:,ofType:,error:) won’t need any modification (if you wrote them correctly); you’ll just use different URLs. In reality, the data of your “cloud” documents is actually stored locally on the device. Any changes are synchronized with the iCloud storage servers in the background, but you always retain a local copy of the data, both for speed and in case the network connection with the cloud is interrupted.

There are some subtle, and not so subtle, differences between local and cloud-based documents. One of the big differences is change. Changes to your cloud documents can occur at any time. The user is free to edit the same document on another device, and network interruptions can delay those changes from reaching your app immediately.

In general, your app observes the UIDocumentStateChangedNotification notification. If the iCloud service detects conflicting versions (local vs. what’s on the server), your document’s state will change to UIDocumentStateInConflict. It’s then up to your app to compare the two documents and decide what to keep and what to discard. You might query the user for guidance, or your app might do it automatically.

Note The Homer the Pigeon app provides an example of cloud-based UIDocument storage and synchronization. Homer, like MyStuff, allows the user to attach images to their saved locations, resulting in too much data to be shared using the ubiquitous key-value store. You can download the source code for Homer at https://github.com/JamesBucanek/Pigeon.

To learn more about iCloud documents, start with the Document-Based App Programming Guide for iOS, which you can find in Xcode’s Documentation and API Reference window. It’s a good read, and I strongly suggest you peruse it if you plan to do any more development involvingUIDocument. The chapters “Managing the Life Cycle of a Document” and “Resolving Document Version Conflicts” directly address cloud storage.

Archive Versioning

When implementing NSCoding, you might need to consider what happens when your class changes. One of the consequences of archiving objects to persistent storage is that the data is—well—persistent. Users will expect your app to open documents created years ago. I’m trying to improve my software all of the time, and I assume you are too. I’m always adding new properties to classes or changing the type and scope of properties. It often means creating new classes and periodically abandoning old ones. All such changes alter the way classes encode themselves and pose challenges when unarchiving data created by older, and sometimes newer, versions of your software.

There are a number of techniques for dealing with archive compatibility. Your newer code might encode its values using a different key. When decoding, your software can test for the presence of that key to determine whether the archive data was created by modern or legacy software. You might encode a “version” value in your archive data and test that version when decoding. Newer software might encode a value in both its modern form and a legacy form so that older software (which knows nothing of the newer form) can still interpret the document data.

There are even techniques for substituting one class for another during unarchiving. (You actually used this in Chapter 14 to substitute your GameScene class for the SKScene class in the archive file.) This can solve the problem of a decoding class that no longer exists. A thorough discussion of these issues and some solutions are discussed in the “Forward and Backward Compatibility for Keyed Archives” chapter of the Archives and Serializations Programming Guide.

Summary

Embracing UIDocument adds a level of modern data storage to your app that users both appreciate and have come to expect. You’ve learned how and where to store your app’s documents. More importantly, you understand the different roles that objects and methods play that together orchestrate the transformation of model objects into raw data and back again. You learned how to construct multifile documents that can be incrementally saved and lazily loaded. Along the way, you learned how to archive objects and create objects that can be archived.

You’ve come a long way, and you should be feeling pretty confident in your iOS aptitude. Adding persistent storage to your apps was really the last major iOS competency you had to accomplish. The next chapter digs into Swift to hone your language knowledge and proficiency.

1Blob is actually a database term meaning Binary Large Object, sometimes written BLOb.

2All property list objects adopt NSCoding. Property list objects are, therefore, a subset of the archivable objects.