Adding the iCloud Infrastructure - Using iCloud Documents and Data - Learning iCloud Data Management (2014)

Learning iCloud Data Management (2014)

Part IV: Using iCloud Documents and Data

13. Adding the iCloud Infrastructure

From here on, you’ll be building various iCloud-enabled implementations of the Reports app that was mentioned previously. The common core of each of these implementations is the shared iCloud data that at various times will be stored on local devices or on iCloud. The implementations may run on OS X or on iOS. And if you think that you need a bit more complexity, the data will be stored using Core Data, or standard Cocoa and Cocoa Touch documents.

What this means is that you will do the same thing over and over in these various contexts and combinations. This chapter helps you set up the software that makes it possible. The software works for an app that is implemented on OS X as well as a corresponding app on iOS. If you want to build only one of those apps, it will also work: you’ll just have an empty folder for the nonsupported operating system. Because it’s almost as easy to build this infrastructure as it is to build a single-OS implementation, it is easier to start out this way so that you keep your deployment options open.

After you set up your Xcode project, you’ll see how to implement an iOS app that uses iCloud to share a very basic document. The app is built around the “Third iOS App: iCloud” document from developer.apple.com. That document shows you how to build an iCloud-enabled iOS app. The app in this chapter borrows the basic document structure, but it reorganizes the project file structure to make it easier to expand the code so that it runs on both iPad and iPhone. In Chapter 16, “Working with OS X Documents,” you’ll see how to expand the code again so that it runs on both iOS devices and Macs.

Also in this chapter, you’ll see how to implement basic iCloud functionality and how to debug iCloud projects using developer.icloud.com. developer.icloud.com is a critical component of your iCloud testing. You can now use the iOS simulator to test your iCloud code. But, as noted previously, to really test iCloud apps, you need to have multiple devices representing each operating system you want to support. That is true for testing of finished apps, but what can you do if you’re just starting out? Do you have to buy lots of new Apple devices?

You can test iCloud with a single iCloud-equipped device. That device can communicate with iCloud, and as a registered developer, you can see your own iCloud container using developer.icloud.com. You can’t test as many combinations and permutations of synchronization with a single device and developer.icloud.com as you can with multiple devices, but you can certainly get a feel for the process and complete some basic testing. This chapter focuses on iOS devices, but if you only have a Mac, you may want to jump ahead to Chapter 16 to implement your app and then use developer.icloud.com as described in this chapter to see what happens.

Exploring the Workspace for the App

By using a workspace, as described in Chapter 11, “Using Xcode Workspaces for Shared Development,” you can set up an iOS target and an OS X target in the same project. There are many ways to do this, but the design described here is the easiest—at least for the author. What you will work toward is a structure of files and folders, as you see in Figure 13.1.

Image

Figure 13.1 Creating a multitarget workspace

There are separate folders for the OS X and iOS projects. A third folder (Shared) contains files that are common to both projects. The workspace itself is placed at the top level of this folder, and the entitlements file, which needs to be part of both targets, is also at the top level of the project folder. (It could also be in the Shared folder.)

That is the structure toward which this chapter is moving. This chapter focuses on what will be the contents of the Shared folder. For simplicity’s sake, it is developed here with a single project—iCloudReports—which is the iOS component.

When you open the workspace in Xcode, the project navigator shows you the files and groups at the left of the workspace window, as always, just as you see in Figure 13.2.

Image

Figure 13.2 Viewing the project in Xcode

Figure 13.2 also shows how you can use the File inspector to check on a file’s path or to modify it. Particularly if you are assembling a workspace like this using projects that have already been created, you may at some time need to update file paths (remember that files shown in red have no files associated with them).

When you add files to a project, you have the choice to copy them into the project if needed. One way to assemble a workspace such as the one shown here is to manually add the files to the appropriate folder (or to create a new project using File, New, Project and set the project location to be the appropriate folder). Once the files are in the right place, add the project to the workspace with the shortcut menu by using Add Files to Project. Just make certain not to use the Copy checkbox. If you do use it, you’ll find that you’ve added a group of the added files in addition to the files themselves within the project. This will not necessarily cause problems, but it will annoy some developers.

Figure 13.3 shows the file and folder structure in the Finder. Compare it to the Xcode view shown in Figure 13.2.

Image

Figure 13.3 Viewing the workspace files in the Finder

A workspace structure like this can help you build both the iOS and OS X apps. The key is knowing what belongs in the Shared folder. Ideally, that code would be used without modifications in both the iOS and OS X versions. Although from time to time you may want to use conditional statements to branch for separate sections for each operating system, many developers feel that the shared code should be “pure” and free of such customizations. You can certainly put the iCloud infrastructure code there. In the case of Core Data, that’s where your data model can be along with the code that will implement the Core Data stack.

Two areas to which you do have to pay attention are document and user interface differences in the two operating systems. They are discussed in the following sections.

Exploring iOS and OS X Document Architecture Differences

There are very different architectures for documents on the two operating systems. This means that you have to take care when sharing documents through iCloud. When you use non-document-based storage architectures such as key-value coding (KVC), Core Data, or property lists, you don’t have to worry about the differences between iOS and OS X except for the fact that certain features (telephony and removable storage, to name two) aren’t available on both platforms.

Perhaps the most significant user interface difference is that documents are not visible to users through the iOS operating system in the way that users can see and manipulate documents in the Finder on OS X. As noted in Chapter 1, “Exploring iCloud and Its User Experience,” iCloud uses apps to organize documents. From the user’s point of view, it may mean reversing the steps to open a document. When you’re in the iCloud world, you open an app, and then you open or create a document for that app. On OS X, you can go to the Finder to locate the document that you want to open. You select the document and choose Open from the File menu or double-click the icon, and the app will open. (Note that this is a simplification because you can use a shortcut menu to choose among various apps that could open a given document. This is only an overview of the process.)

What is significant is that on OS X the management of opening a document or of saving it is done using standard Cocoa tools. The Finder takes care of showing you the documents that could be opened.

On iOS, because you start by launching the app you want to use, each app is responsible for displaying the files it can open. Thus, managing an app’s documents is the app’s job rather than the job of the operating system.

Dealing with UI Differences

Although Cocoa and Cocoa Touch have common roots, there are important differences. Also, Cocoa Touch was developed years after Cocoa, so some changes that were difficult to implement on an incremental basis in Cocoa were added from the start in Cocoa Touch (sandboxing is one of these). Among the most important differences are those that stem from document management, as described in the previous section, as well as those that are related to the absence of a menu bar on iOS.

Even when the logic of the interface is the same on both iOS and OS X versions of an app, the mechanics often differ (posing an alert is a good example). Trying to make OS X and iOS versions of an app similar can make life easier for users than having to learn two different interfaces; however, in practice, users seem to prefer interfaces that look “right” for the device over those that look “wrong” on both types of devices.

Designing the Shared App Folder Structure

The challenge for a developer is to create an app structure that allows as much common code as possible but also provides easy implementation of device-specific code. That is the strategy used here. As you have seen, there are two separate projects in the workspace—one for iOS and one for OS X. On disk, each project’s files are in a separate Finder folder. A Shared folder contains the files that are shared: these files are added to both of the projects in a Shared group for each of the projects. Although groups in Xcode use folders as icons, remember that they are not Finder folders. As long as you keep in mind that the shared files exist in a single place on disk (the Shared folder) while they appear in two places in the workspace (the Shared groups in both projects), all will be well. Because these are shared files, editing them from either project edits them in the other.

One prime candidate for the Shared folder will be everything that has to do with iCloud itself. You will share a ubiquity container between the OS X app and the iOS app, so it makes sense to share the code that manipulates it. Unfortunately, that is easier said than done. Apple has an excellent tutorial, “Your Third iOS App: iCloud,” that introduces you to iCloud. That app is a great starting point, but it runs only on iPhone. The app shown in this chapter runs on both iPhone and iPad. Furthermore, it is factored so that the common iCloud code can be placed in the Shared folder, while the OS-specific code (mostly UI interactions) is placed in the relevant projects. In addition, the structure of the Shared folder is set up with an eye to expanding the app to OS X, which you’ll do in Chapter 16.

Checking Out the End Result

The app in this chapter is built on the Master-Detail Application template for iOS. By the end of the chapter, you’ll be able to create documents on an iPhone, as shown in Figure 13.4.

Image

Figure 13.4 Creating and selecting documents on iPhone

You’ll also be able to run the same app on an iPad, as shown in Figure 13.5.

Image

Figure 13.5 Running the app on iPad

One of the biggest differences has to do with the split view controller on iPad: there is no comparable feature on iPhone. Thus, as you see in Figure 13.5, you can see the list of documents as well as the content of the selected document at the same time. On iPhone, a navigation interface lets you move from the list (the master view controller) to a detail controller, as you see in Figure 13.6.

Image

Figure 13.6 The iPhone version uses a navigation interface

Scoping the Project

This chapter’s app is not complete: it is designed to get you started with an app that uses iCloud across multiple iOS devices. You’ll add OS X functionality in Chapter 16.

Furthermore, it uses the same simplification that is used in “Your Third iOS App: iCloud.” It writes the document data directly to iCloud. For a real project, you would probably write to a local directory and then move that local data to iCloud using one of the techniques described in the following chapters. That strategy allows for sharing (through iCloud) as well as continued operation if iCloud is not available.

Managing local and iCloud copies of data is an important part of iCloud development, but it does add a complicating factor, which you don’t need at this point. You will see how to help users manage conflicts in versions of their data, but that is one of the areas where the differing document structures in OS X and iOS come into play. That is why that discussion is deferred to Chapters 15 and 16.

Behind the scenes in this project, the document data is stored in a UIDocument in the most minimal form possible—a text string.

Debugging iCloud Apps with developer.icloud.com

When you talk about debugging an app on a Mac or an iOS device, you usually talk about running it to make certain it does what you want it to do (and doesn’t do what you don’t want it to do.) That’s very simple, but when you’re working with iCloud, you have some additional challenges. Specifically, how do you see what the app is doing when what it is doing is happening in iCloud?

You can examine your iCloud storage using developer.icloud.com if you are a registered developer. Log in with your developer ID, and you’ll see your iCloud storage with its Documents folder, as shown in Figure 13.7. There may be some additional folders, but for now Documents is what you are interested in.

Image

Figure 13.7 Viewing your iCloud storage

Open the Documents folder shown in Figure 13.7, and, as you see in Figure 13.8, each of the apps that you use in iCloud has its own folder.

Image

Figure 13.8 Each app has its own iCloud folder

Within the folder for the app that will be developed in this chapter, you can see all of its documents. Compare Figure 13.8 with Figures 13.4 and 13.5 shown previously.

Just as in the Finder, you can select a document and see its size and modification date. Also note that you can use the button at the right of Figure 13.9 or the down-pointing arrow at the top to download a file from iCloud. This lets you open the file on your Mac to check its contents.

Image

Figure 13.9 Viewing your iCloud documents

Note that these are debugging tools only accessible by a registered developer. Users can’t see their iCloud data in this way.

As you can see in Figure 13.10, files outside of the Documents folder may or may not be visible to users (it’s your choice). You can use the Home button, as shown in Figure 13.10, to move outside the Documents folder to see files and folders you have created inside the app’s ubiquity container.

Image

Figure 13.10 Moving outside the iCloud Documents folder

If you already have an iCloud-enabled app that works with documents, you can open developer.icloud.com to see those documents. One of the most important things to learn about iCloud is that it is an asynchronous process. If you have one of the iWork apps, here is an important experiment to conduct:

1. Open developer.icloud.com for your developer ID.

2. Go to the apps list, as shown in Figure 13.8.

3. Run an iWork app and create a new document.

4. Save it to iCloud, as shown in Figure 13.11.

Image

Figure 13.11 Saving an iWork app to iCloud

5. Watch to see the document appear in iCloud.

If this is the first document you’ve saved in iCloud from this app, the app will be added to the list of apps shown previously in Figure 13.8. Repeat this process periodically to see how the time varies for iCloud updates. Because there is a local copy of your iWork document, it doesn’t matter that the upload isn’t simultaneous because you can continue working.

Building the App

Before you start to build the app, go to developer.apple.com and create an App ID for iCloud Reports for development on iOS. (Yes, you can build the App ID before you start coding the app.)

Now it’s time to build the app. Here are some steps to get started. Because of the shared folder, you may have to do a bit of rearranging, but once it’s done, things will be easier to work with.

1. Create a new workspace (see Chapter 11, “Using Xcode Workspaces for Shared Development”).

2. Create a new project based on the Master-Detail Application template called iCloud Reports.

3. If you want to use the Shared folder structure described previously, create that folder inside the project folder. (Remember, you’re creating the Shared folder using the Finder.)

4. Make certain that it has a universal interface and that it does not use Core Data.

5. Turn on iCloud in Capabilities for the project. An entitlements file will be created for you and added to the project, as shown in Figure 13.12.

Image

Figure 13.12 Using an entitlements file

6. Depending on your project, you may also want to use iCloud and Key-Value Store.

7. Accept the default ubiquity container.

If you want to look inside the entitlements file, you’ll see what Xcode has done for you, as shown in Figure 13.12. There is nothing for you to do, but you might want to see what has been set up.

You will be adding and modifying code in the template classes. The changes are discussed in the following sections.

Creating the Shared Folder

The Shared folder is a good place to start coding with Xcode because it is not dependent on any of the other classes, although other classes are dependent on it. (Notifications make this possible.) There will be two classes in the Shared folder:

Image Constants is a convenience class.

Image SharediCloudController is just what its name says: it provides iCloud support for both of your projects.

Constants.h

Constants.h is a class containing constants for the project. There are many ways to provide constants across the project. The simplest is an ordinary #define. Creating a class is a little more work (but not much), and it can provide some improvements in turnaround time when developing a large project. Once you have declared a constant in the .h file, if you change its value in the .m file, that may require fewer other classes to be recompiled.

You can see the structure of the header in Listing 13.1.

Listing 13.1 Constants.h


//
// Constants.h
// OS X iCloud Reports
//
// Created by Jesse Feiler on 3/17/13.
// Copyright (c) 2013 Champlain Arts Corp. All rights reserved.
//

extern NSString *const SharediCloudControllerDidChangeDataNotification;

extern NSString *const SharedReportFileExtension;
extern NSString *const DocumentsDirectoryName;

extern NSString *const DisplayDetailSegue;
extern NSString *const ReportEntryCell;
extern NSString *const KeyForCurrentUbiquityToken;


Constants.m

The .m file is shown in Listing 13.2. At this point, you may want to simply create the files (using File, New File and the Objective-C template); you can add the constants as you need them (when their names will make more sense).

Listing 13.2 Constants.m


//
// Constants.h
// OS X iCloud Reports
//
// Created by Jesse Feiler on 3/17/13.
// Copyright (c) 2013 Champlain Arts Corp. All rights reserved.
//

#import "Constants.h"

NSString *const SharediCloudControllerDidChangeDataNotification =
@"iCloudDataChanged";

NSString *const SharedReportFileExtension = @"reportdoc";
NSString *const DocumentsDirectoryName = @"Documents";

NSString *const DisplayDetailSegue = @"DisplayDetailSegue";
NSString *const ReportEntryCell = @"ReportEntryCell";

NSString *const KeyForCurrentUbiquityToken =
@"com.champlainarts.iCloud.UbiquityIdentityToken";


SharediCloudController.h

SharediCloudController.h is the key to the Shared folder: as its name implies, it is a shared iCloud controller (in the sense of model-view-controller). If you compare the code in this section and the following one with “Your Third iOS App: iCloud,” you’ll notice the code is similar in many places, but it is reorganized so that it can be more easily shared. For example, user interaction is managed by the classes in the iOS project (and, later on, the OS X project). This means that you can’t test for a condition and report back immediately. You have to be asked to return a value and then the calling class can report.

One of the consequences of this type of structure that is very common in Cocoa is that you often do not execute code sequentially. iCloud relies heavily on threading to manage its processing. As soon as there are multiple threads, you can’t just execute the next line of code, because you can’t really tell what the next line of code will be. Thus, you post a notification and another part of your code responds to it. If you aren’t familiar with notifications, you will be by the end of the book. Rest assured that one of the reasons they work and are used so extensively is that they really are simple, as you’ll see.

Listing 13.3 shows the SharediCloudController.h file. Perhaps the most important thing to take away from the interface is that the controller manages the list of documents stored in iCloud. As noted previously, this is needed only on iOS, but because the shared iCloud controller will be used to manage all iCloud data, it does so here as well.

Listing 13.3 SharediCloudController.h


//
// SharediCloudController.h
// OS X iCloud Reports
//
// Created by Jesse Feiler on 3/17/13.
// Copyright (c) 2013 Champlain Arts Corp. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface SharediCloudController : NSObject

- (NSInteger)numberOfDocuments; //1
- (NSString*)stringForDocumentNameAt: (NSInteger)index; //2
- (id)documentURLAt: (NSInteger)index; //3

- (NSString *)newUntitledDocumentName; //4
- (void)addDocumentURL: (NSURL *)newDocumentURL; //5
- (void)deleteDocument: (NSURL *)deleteDocumentURL; //6


- (void)checkOnQuery; //7

- (void)manageiCloudIdentityChange; //8
- (void)deleteDocumentsFromUI; //9


@end


1 Although documents are managed by the controller, certain aspects of them are exposed in the controller’s interface. The number of documents is one such aspect.

2 Once documents are loaded, they can be identified by number (this number can change over time). This lets you get the name of the nth document. You’ll see why it needs to be exposed in Listing 13.4.

3 You can get the URL for the nth document.

4 When creating a new document, you need to give it a name that, by default, includes a number (as in Document 2, Document 3).

5 When a document is created, you can add its URL to the documents list even though you can’t see the list.

6 You need to be able to delete a document as well as add it.

7 checkOnQuery is a debugging method. Particularly when you’re dealing with asynchronous processing, you can’t step through code with a breakpoint, so it sometimes helps to put in a debugging method in which you can place a breakpoint.

8, 9 These methods will be used to respond to a notification or the app entering or leaving the active state.

SharediCloudController.m

As you can see in Figure 13.13, the methods in this file are separated into five sections as well as a class extension at the top of the file.

Image

Figure 13.13 SharediCloudController.m has five sections.

The sections of code are:

Image Initialization: This is standard initialization for any class along with starting iCloud access. You will probably reuse this code pretty much as-is in other projects. The customization that you will need is done in the Xcode Capabilities tab when you specify the entitlements file and the ubiquity container.

Image Managing iCloud Access: This is the code that manages access to iCloud, changes to iCloud availability, and changes to the iCloud account. It, too, is fairly generic.

Image Managing iCloud Data: This code also is fairly generic. You search your iCloud container for the data you’re interested in using a query that observes changes. Reusing this code generally requires specifying the objects you’re looking for in your container. When they’re found, they are added to a local array (documents).

Image Managing Documents Array: This is pretty much specific to your app as you add and remove items to and from the array.

Image Debugging: This is a debugging method that is placed in its own section to help you debug the multithreaded code.

Listing 13.4 shows the initialization section. initializeiCloudAccess is the key component here. It makes the connection to iCloud from your app. Because that can take some time, it is critical that you do not call it from your main thread where it could block your app’s processing. In this code, messages are written out to the log about iCloud status. You’ll see in Chapters 15 and 16 how to create more user-friendly messages.

Following the block, you see the registration for NSUbiquityIdentityDidChangeNotification, which lets you know that the iCloud account has changed (possibly to no account if the user has signed out). In addition, you need to keep track of when the app enters and leaves the active state. There are stub methods in AppDelegate.m that you can use to capture this information. In this section, you will see the methods of the shared iCloud controller that are used to react to those changes.

The reason for tracking the active status of the app is to be able to handle what is a rare but critical occurrence. If the user is running with one iCloud account (call it A), it’s easy to log out of account A and log in to account B. On an iOS device, you do that by switching to Settings and making the change. When the user switches to Settings, by definition your app becomes inactive. By tracking the active status of the app in the app delegate, you can capture that account change when the app becomes active again.

Many users do not change their iCloud accounts at all, but you as a developer are likely to change iCloud accounts fairly frequently during testing, so you definitely need this code to function properly. The strategy employed here is very simple: when the app becomes inactive, the list of iCloud documents is emptied, and when it becomes active, it is reloaded with whatever iCloud account is in use at that moment. Depending on your app, you may have other strategies you can use, but you have to prepare for account changes that may happen out of your view.

Listing 13.4 Class Extension and Initialization


//
// SharediCloudController.m
// OS X iCloud Reports
//
// Created by Jesse Feiler on 3/17/13.
// Copyright (c) 2013 Champlain Arts Corp. All rights reserved.
//

#import "SharediCloudController.h"
#import "Constants.h"

@interface SharediCloudController () //1
@property NSMutableArray *documents;
@property NSMetadataQuery *query;
@end

@implementation SharediCloudController

#pragma mark - initialization

-(id)init {
if ( self = [super init] ) {
[self initializeiCloudAccess]; //2

if (!_documents)
_documents = [[NSMutableArray alloc] init]; //3

[self setupAndStartQuery];
}
return self;
}

- (void)initializeiCloudAccess { //4
dispatch_async (dispatch_get_global_queue
(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
if ([[NSFileManager defaultManager]
URLForUbiquityContainerIdentifier:nil] != nil)
NSLog (@"Got iCloud with URLForUbiquityContainerIdentifier %@ \n",
[[NSFileManager defaultManager]
URLForUbiquityContainerIdentifier:nil]);
else
NSLog (@"No iCloud");
});

[[NSNotificationCenter defaultCenter] //5
addObserver:self
selector:@selector (manageiCloudIdentityChange)
name:NSUbiquityIdentityDidChangeNotification
object:nil];

[self manageiCloudIdentityChange];
}


1 The class extension declares two properties. The use of class extensions in this way is a fairly recent development. Declaring properties instead of instance variables is even more recent. As in the interface, the property declaration causes the backing variable to be created (the property name is prefixed by an underscore). You can access the backing variable directly with its name or by using dot syntax as with any property.

2 The app delegate creates an instance of SharediCloudController if necessary when a client asks for it (the code is shown later in this chapter in the AppDelegate.m section). The first request for it comes from MasterViewController in the initWithCoder: method. Thus, the instance of SharedCloudController is created at the beginning of the app’s processing.

3 Create _documents if necessary. It remains present even if all entries are removed from it (as when the iCloud account changes).

4 This is the connection to iCloud and your ubiquity container. This code can generally be used as-is without further customization. By passing in nil for URLForUbiquityContainerIdentifier, you cause Cocoa to pick the first ubiquity table from thecom.apple.developer.ubiquity-container-identifiers array. That is set automatically when you set up your ubiquity container in your project (see Figure 13.10).

5 Register for NSUbiquityIdentityDidChangeNotification and call manageiCloudIdentityChange to get started.

Managing iCloud Access

As part of the initialization of the shared iCloud controller, it registers for notification of changes in the iCloud account, as you saw in Listing 13.4. Listing 13.5 shows the code that handles such notifications.

This is standard code that you can reuse. It relies on obtaining the iCloud token and storing it in NSUserDefaults. The first step is to get the previous token out of defaults, and then you obtain the new token. (Remember that, unlike the code in Listing 13.4 that gets the ubiquity container URL, you can get the token on the main thread because it’s a very fast operation.

The heart of the method is a nested if statement that results in the following actions depending on the values and existence of the current and previous tokens:

Image No current token: Remove previous token so that there is now no token for the KeyForCurrentUbiquityToken key in user defaults. If you support non-iCloud processing, you should capture changes so they can be uploaded at a later time.

Image Current token that doesn’t match previous token: Update the user interface and the _documents array with current token’s iCloud data. This is done with setupAndStartQuery, shown later in Listing 13.6.

Image Current token does match previous token: This means the user is continuing with the same account. This case is also handled in setupAndStartQuery.

Listing 13.5 Responding to a Change in iCloud Identity


- (void)manageiCloudIdentityChange {
id previousiCloudToken = [NSKeyedUnarchiver
unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults]
objectForKey:KeyForCurrentUbiquityToken]];
id currentiCloudToken = [[NSFileManager defaultManager]
ubiquityIdentityToken];
BOOL sameAccount = [previousiCloudToken isEqual: currentiCloudToken];
NSLog (@" %hhd %@ %@", sameAccount, previousiCloudToken,
currentiCloudToken);
if (currentiCloudToken) {
if (!sameAccount) {
NSData *newTokenData = [NSKeyedArchiver
archivedDataWithRootObject: currentiCloudToken];
[[NSUserDefaults standardUserDefaults]
setObject: newTokenData
forKey: KeyForCurrentUbiquityToken];
}
[self setupAndStartQuery]; //6
} else {
[[NSUserDefaults standardUserDefaults]
removeObjectForKey: KeyForCurrentUbiquityToken];
[self deleteDocumentsFromUI];
}
}


6 This method finds iCloud documents and updates the interface. See Listing 13.6 for the code.

Managing iCloud Data

The documents array in this app contains the names of all the documents in the ubiquity container. Those names are NSURL object instances. Given the NSURL, you can then open a specific document and retrieve its data (the data is an NSString for simplicity’s sake in this case).

The process of managing iCloud data consists of three related methods that are shown in Listing 13.6. They use the NSMetaDataQuery class that was introduced in OS X Tiger (10.4) as part of Spotlight in 2005. You create the query in one method. You then start the query in another method and register to be notified of its status—either a batch of updates or completion. The third method fires when one of the notifications—updates or completion—is triggered. This structure is common when you are managing iCloud data.

You can see the evolution of NSMetaDataQuery in the constants you use to set the scope of the query. Here are the five current constants; the last two were added recently to support iCloud. They refer to the Documents folder in the app’s container or to all files outside the Documents folder:

NSString *const NSMetadataQueryUserHomeScope;
NSString *const NSMetadataQueryLocalComputerScope;
NSString *const NSMetadataQueryNetworkScope;
NSString *const NSMetadataQueryUbiquitousDocumentsScope;
NSString *const NSMetadataQueryUbiquitousDataScope;

You start the query from the main thread, and it continues processing until you stop it (by default it checks once a second). Queries like this are called live queries. You can also run static queries, which run the query and then finish. When the iCloud account changes (either to another account or by turning off iCloud), the query is set to nil. It will be recreated as needed with the new account. Both for your own testing as well as for user support, it’s important to note that Airplane mode will leave the existing iCloud account in place but simply turn off the radio. You can simplify your testing as well as users’ experiences with your app if you remind yourself and them that signing out of iCloud isn’t necessary to turn off the radio.

Listing 13.6 Managing iCloud Data with NSMetaDataQuery


#pragma mark - managing iCloud data

-(NSMetadataQuery *)textDocumentQuery {
NSMetadataQuery *aQuery = [[NSMetadataQuery alloc] init];
if (aQuery) {
// search
[aQuery setSearchScopes: [NSArray arrayWithObject:
NSMetadataQueryUbiquitousDocumentsScope]]; //7

// add predicate
NSString * filePattern = [NSString stringWithFormat:@"*.%@",
SharedReportFileExtension]; //8
[aQuery setPredicate: [NSPredicate predicateWithFormat: @"%K LIKE %@",
NSMetadataItemFSNameKey, filePattern]];
}
return aQuery;
}

- (void)setupAndStartQuery {
// create if needed
if (!_query)
_query = [self textDocumentQuery]; //9

// register for query notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector
(processFiles:) name:NSMetadataQueryDidFinishGatheringNotification
object:nil]; //10
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector
(processFiles:) name:NSMetadataQueryDidUpdateNotification
object:nil]; //11

// start the query
[_query startQuery]; //12

}

-(void)processFiles: (NSNotification*)aNotification {
NSMutableArray *discoveredFiles = [NSMutableArray array];

// disable updates
[_query disableUpdates]; //13


NSArray *queryResults = [_query results]; //14

for (NSMetadataItem *result in queryResults) {
NSURL *fileURL = [result valueForAttribute: NSMetadataItemURLKey];
NSNumber *aBool = nil;

// don't include hidden files
[fileURL getResourceValue: &aBool forKey:NSURLIsHiddenKey
error:nil]; //15
if (aBool && ![aBool boolValue])
[discoveredFiles addObject: fileURL];
}

// update list
[_documents removeAllObjects]; //16
[_documents addObjectsFromArray: discoveredFiles];

//reenable updates
[_query enableUpdates]; //17

[[NSNotificationCenter defaultCenter]
postNotificationName: SharediCloudControllerDidChangeDataNotification
object:nil userInfo:nil]; //18

}


7 Create the query and set the search scope. Take care with Xcode code completion: NSMetadataQueryUbiquitousDocumentsScope can easily be completed as NSMetadataQueryUbiquitousDataScope or something like that if you’re not careful.

8 Create a string to use in the predicate to search for all files with the SharedReportFileExtension constant. If you haven’t added it to Constants.h and Constants.m, do so now. Its value in this example is reportdoc and set the predicate for the query. If you follow this structure, you customize the file extension in line 7 and then use this line of code without modification.

9 If needed, create the query using the textDocumentQuery method.

10 Observe NSMetadataQueryDidFinishGatheringNotification with a selector for the processFiles method (which follows).

11 Similarly, observe NSMetadataQueryDidUpdateNotification with the processFiles method.

12 Start the query.

13 When processFiles is run as a result of a notification, you must disable updates to the query until processFiles is complete. At that point, you enable updates. (See line 17.)

14 Load query results into a local array so you can iterate through it.

15 For each file, check to see if it should be hidden. In getResourceValue: forKey:error, note that &aBool will return the key value for you to check. If it is not hidden, add it to another local array, discoveredFiles.

16 Replace _documents with discoveredFiles. Note that this process simply replaces all the objects. This means that you don’t have to worry about deletes—you just take what the query has discovered and use it going forward.

17 Enable updates. (See line 13.)

18 Post a notification that you have found updates using SharediCloudControllerDidChangeDataNotification. If you haven’t yet declared it in Constants, add it now.

As you can see in Listing 13.6, the query is created and started as necessary when the iCloud account changes (see Listing 13.5). It’s important that it be created properly after an account change, and the easiest way to do that is to set it to nil when the account logs out. This is handled at the end of the if statement in Listing 13.5. If there has been a change to the account—either by logging out of iCloud or choosing a new account—the method shown in Listing 13.7 is called to update the UI. It sets the query to nil and empties the documents array, which will be refilled the next time a query is created and run. More details on this process are found later in this chapter in the discussion of the master view controller.

Listing 13.7 Turning Off the Query


- (void)deleteDocumentsFromUI {
_query = nil;

[_documents removeAllObjects]; //19
[[NSNotificationCenter defaultCenter]
postNotificationName: SharediCloudControllerDidChangeDataNotification
object:nil userInfo:nil];
}


19 Empty _documents and post a notification to update the interface.

Managing the Documents Array

At this point, you have discovered the objects in the ubiquity container. The methods shown in Listing 13.8 let you manage them. There is no iCloud-specific code in this section, so the methods are not annotated because you have probably seen this code (or code much like it) many times. Remember that _documents is an array of the NSURL objects representing the actual documents.

Listing 13.8 Managing the Documents Array with Names and URLs


#pragma mark - managing documents array

-(NSString*)stringForDocumentNameAt: (NSInteger)index { //20
if ([_documents count] > 0)
{
NSURL *theURL = [_documents objectAtIndex: index];
NSLog (@" %@", theURL);
return [[theURL lastPathComponent] stringByDeletingPathExtension];
}
return nil;
}

-(NSURL *)documentURLAt: (NSInteger)index { //21
//NSLog (@" %ld", (long)index);
if ([_documents count] > 0)
{
NSURL *theURL = [_documents objectAtIndex: index];
//NSLog (@" %@", theURL);
return theURL;
}
return nil;
}

- (NSInteger)numberOfDocuments { //22
return [_documents count];
}

- (NSString *)newUntitledDocumentName { //23
NSInteger docCount = 1;
NSString *newDocName = nil;

newDocName = @"new doc";
BOOL done = NO;
while (!done) {
newDocName = [NSString stringWithFormat: @"Note %d.%@", docCount,
SharedReportFileExtension];
// check for dups
BOOL nameExists = NO;
for (NSURL *aURL in _documents) {
if ([[aURL lastPathComponent] isEqualToString: newDocName]) {
docCount ++;
nameExists = YES;
break;
}
}

// if not found, exit WHILE
if (!nameExists)
done = YES;
}
return newDocName;
}

- (void)addDocumentURL: (NSURL *)newDocumentURL { //24
[_documents addObject: newDocumentURL];
}
- (void)deleteDocument: (NSURL *)deleteDocumentURL {

dispatch_async (dispatch_get_global_queue
(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //25
NSFileCoordinator *fc = [[NSFileCoordinator alloc]
initWithFilePresenter:nil];
[fc coordinateWritingItemAtURL:deleteDocumentURL
options:NSFileCoordinatorWritingForDeleting error:nil
byAccessor:^(NSURL *newURL) {
NSFileManager *fm = [[NSFileManager alloc] init];
[fm removeItemAtURL: newURL error:nil];
}];
});

[_documents removeObject: deleteDocumentURL]; //26

}

#pragma mark - debugging

- (void)checkOnQuery { //27
if (_query) {
NSLog (@" %lu", (unsigned long)[_query resultCount]);
NSLog (@" %lu", (unsigned long)[_query isStarted]);
NSLog (@" %lu", (unsigned long)[_query isGathering]);
NSLog (@" %lu", (unsigned long)[_query isStopped]);
[_query startQuery];
}


20 Get a document name.

21 Get a document URL.

22 Get the number of documents.

23 Get a unique name for a new document.

24 Add a new document to the array. Note that this only happens when you create a new document. If someone else creates a new document in iCloud, you get it in processFiles the next time it runs.

25 Switch off the main queue for the file handling steps to actually delete the document.

26 Delete the document from the _documents array.

27 This is only for debugging. You can comment it out or delete it. You can invoke it as the last line in processFiles. Set a breakpoint on the first line of this method if you want to track your query’s progress.

The shared iCloud controller is the major piece of code you need for this app. The sections that follow show how you’ll use it.

Creating the App’s Classes

There are three basic classes in the Master-Detail Application:

Image AppDelegate

Image MasterViewController

Image DetailViewController

Along with the storyboards, they are discussed in this section. The emphasis throughout is on the iCloud features rather than the basic Objective-C and the Master-Detail Application template.

AppDelegate

There’s not much to do with the AppDelegate class. You need to instantiate a SharediCloudController instance and assign it to a property in the app delegate.

AppDelegate.h

You need a forward reference to the SharedCloudController class, and you need to declare a property for the instance you use in the app. The top of AppDelegate.h is shown in Listing 13.9.

Listing 13.9 AppDelegate.h


#import <UIKit/UIKit.h>

@class SharediCloudController; //1

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (nonatomic, readonly) SharediCloudController *iCloudController;//2

@end


1 Forward reference for the controller class.

2 Declare a property for the app delegate to use for the iCloud controller.

AppDelegate.m

You need to instantiate an instance of the iCloud controller. Listing 13.10 shows AppDelegate.m. As you saw in the previous section, the init method of Shared-iCloudController handles the startup processing for iCloud that often is placed in the app delegate directly. The unused stubs of methods that are found in the template are omitted from Listing 13.10.

Listing 13.10 AppDelegate.m


#import "AppDelegate.h"

#import "SharediCloudController.h"

@interface AppDelegate
@property SharediCloudController *_iCloudController; //1
@end

@implementation AppDelegate

#pragma mark - iCloud

- (SharediCloudController *)iCloudController {
if (!iCloudController) { //2
iCloudController = [[SharediCloudController alloc] init];
}
return _iCloudController;

- (void)applicationWillResignActive:(UIApplication *)application { //3
[_iCloudController deleteDocumentsFromUI];
}

// stub code omitted

- (void)applicationDidBecomeActive:(UIApplication *)application { //4
[_iCloudController manageiCloudIdentityChange];
}

// stub code omitted


}


1 In the class extension, SharediCloudController can be declared as a property. Xcode will create the backing variable, and you’ll be able to use dot syntax or the underscore name of the backing variable (_iCloudController) when referencing it from this file.

2 This syntax is very common. If a necessary object isn’t found, create it.

3 Clear out user interface in case the iCloud account will be changed.

4 Check for a new iCloud account and, if necessary, update the token in user defaults and the UI.

MasterViewController

The master view controller manages the list of detail objects. That list is the major focus of the iCloud integration.

MasterViewController.h

There are four main tasks in managing the list of documents:

Image Initializing the master view controller: As always, you need to set up the instance of a class at the beginning.

Image Creating a new document: This is where you will create a new document and store it both in iCloud and in the private documents array. In practice, you will probably also store a local copy in the app’s sandbox, but that is covered in Chapters 15 and 16.

Image Deleting an existing document: You need to be able to delete documents at will. They will need to be deleted from iCloud as well as from the private documents array. In Chapters 15 and 16, you’ll see how to also delete them from the local store.

Image Displaying the list of documents: You need to be able to display new and old documents. (Remember, that’s the main function of the master view controller.)

Initializing the master view controller means storing a reference to the app delegate’s instance of the iCloud controller as well as registering for necessary notifications. Listing 13.11 shows the header file (MasterViewController.h).

Listing 13.11 Setting Up the Master View Controller Header


#import <UIKit/UIKit.h>

@class DetailViewController;

@interface MasterViewController : UITableViewController

@property (strong, nonatomic) DetailViewController *detailViewController;
@property (weak, nonatomic) IBOutlet UIBarButtonItem *addButton;

- (IBAction)addDocument:(id)sender; //1

@end


1 This will implement the button to add a new document to the master view controller.

MasterViewController.m

The beginning of MasterViewController.m is shown in Listing 13.12.

Listing 13.12 Implementing the Master View Controller


#import "MasterViewController.h"
#import "Constants.h"
#import "DetailViewController.h"
#import "ReportDocument.h"
#import "AppDelegate.h"
#import "SharediCloudController.h"

@interface MasterViewController ()

@property (weak,nonatomic) SharediCloudController* iCloudController; //1

@end

@implementation MasterViewController

- (id)initWithCoder:(NSCoder*)aDecoder //2
{
if(self = [super initWithCoder:aDecoder])
{
AppDelegate *delegate =
(AppDelegate*)[[UIApplication sharedApplication]
delegate];
_iCloudController = [delegate iCloudController]; //3
}

[[NSNotificationCenter defaultCenter] addObserver:self //4
selector:@selector (reloadData)
name:SharediCloudControllerDidChangeDataNotification object:nil];

return self;
}

- (void)awakeFromNib //5
{
[super awakeFromNib];

self.navigationItem.leftBarButtonItem = self.editButtonItem;

[self.tableView reloadData]; //6

}

- (void)reloadData {
[self.tableView reloadData]; //7
}


1 The the shared iCloud controller is declared as a property inside the class extension.

2 The method to override is initWithCoder rather than a basic init method. This method is used to initialize an object from a stored (or coded) value. This enables the instantiated object to have the various values that have been set in another file—typically a nib, xib, or storyboard file. initWithCoder is how all of your settings for your object move from the storyboard to the runtime instance.

3 Set the value of the property to the app delegate’s instance of the shared iCloud controller.

4 Register for notifications that may be sent from the shared iCloud controller. If they are received, reloadData will be called to update the list of documents in the master view controller. Note once again the message-based coupling of asynchronous processes.

5, 6 awakeFromNib is a standard method to make certain that the app and its interface are up to date. Remember, changes to the list of documents may have happened in iCloud, so it is worthwhile to reload the data.

7 This is the standard way to reload data in a table view. Note that the view controller just hands off the reloading to its table view.

Several methods are omitted from this listing because they are common split view controller methods that are well documented on developer.apple.com. They are viewDidLoad and didReceiveMemoryWarning, as well as the Table View section with methods such asnumberOfSectionsInTableView. They are in the downloadable code.

You need to create a method for adding a new document to the Master-Detail Application template for your app. That’s the focus of this section. The document will be added directly to iCloud as well as to the internal documents array. (As noted previously, in real life you would usually add the new document to a local persistent store of some kind, but this is a simplification for the first example.) The method is exposed in the .h file. The implementation code is shown in Listing 13.13.

Listing 13.13 Wiring the Add Button


#import <UIKit/UIKit.h>

@class DetailViewController;

@interface MasterViewController : UITableViewController

@property (strong, nonatomic) DetailViewController *detailViewController;
@property (weak, nonatomic) IBOutlet UIBarButtonItem *addButton;

- (IBAction)addDocument:(id)sender; //1

@end


1 Create the declaration of the new addDocument method. The easiest way is to edit your storyboard with the Assistant in Xcode. Use the existing add button in the template, but rewire it by dragging from it to the MasterViewController.h file and creating the action. This line of code will be generated for you automatically.

In the .m file, you implement addDocument as well as the other methods that manage the list of documents. In Listing 13.14, you see the process of adding the file.

As is so often the case with iCloud, you’re dealing with asynchronous processing in several threads. You start by sending the basic processing to a background queue (this created the new document). You then send a block to the main queue to update the user interface. (The user interface cannot be updated from a background queue.) As you’ll notice in Listing 13.14, the code in the background queue sets up some variables that are then dispatched to the main queue (such as newDocumentURL). Thus, although you have two queues in action, the second doesn’t get scheduled until the first has set the variables. This is the common structure for this type of work. (This is basically the same code used in “Your Third iOS App: iCloud” on developer.apple.com; however, in this version, SharediCloudController manages the documents array instead of having it managed by the master view controller. This can provide for greater flexibility in reusing the code.

Listing 13.14 Creating a New Document


- (NSString *)newUntitledDocumentName { //1
return [_iCloudController newUntitledDocumentName];
}

- (IBAction)addDocument: (id)sender {

self.addButton.enabled = NO; //2

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{ //3
NSFileManager *fm = [NSFileManager defaultManager]; //4
NSURL *newDocumentURL = [fm URLForUbiquityContainerIdentifier:nil]; //5
newDocumentURL = [newDocumentURL
URLByAppendingPathComponent:DocumentsDirectoryName isDirectory:YES];
newDocumentURL = [newDocumentURL URLByAppendingPathComponent:
[self newUntitledDocumentName]];

dispatch_async (dispatch_get_main_queue(), ^{ //6
// update documents
[_iCloudController addDocumentURL: newDocumentURL];

// update UI //7
NSIndexPath *newCellIndexPath = [NSIndexPath
indexPathForRow:([_iCloudController numberOfDocuments] - 1)
inSection:0];
[self.tableView insertRowsAtIndexPaths:
[NSArray arrayWithObject:newCellIndexPath]
withRowAnimation: UITableViewRowAnimationAutomatic];
[self.tableView selectRowAtIndexPath: newCellIndexPath animated:YES
scrollPosition:UITableViewScrollPositionMiddle];

UITableViewCell *selectedCell = [self.tableView
cellForRowAtIndexPath: newCellIndexPath];
if ([[UIDevice currentDevice] userInterfaceIdiom] ==
UIUserInterfaceIdiomPad) { //8
// set selectedItem for iPad
NSIndexPath *cellPath = [self.tableView indexPathForSelectedRow];
NSURL *theURL = [_iCloudController documentURLAt:[cellPath row]];
_detailViewController.detailItem = theURL; //9
} else if ([[UIDevice currentDevice] userInterfaceIdiom] ==
UIUserInterfaceIdiomPhone) {
// start segue for iPhone
[self performSegueWithIdentifier: DisplayDetailSegue
sender: selectedCell]; //10
};

//re-enable add button
self.addButton.enabled = YES; //11

}); // main queue
}); // background queue
}


1 Ask the shared iCloud controller to create a new document name. It will be used in line 4.

2 Disable updating while the original update is being processed.

3 This is the beginning of the block being sent to the background queue.

4 Get the default file manager. This and the following two lines could be merged into a single, rather inscrutable statement.

5 Build the new file name using the ubiquity container name, the documents directory name (from Constants.h), and the document name created in line 1. To place the document somewhere else, replace Documents-DirectoryName with another location.

6 Move back to the main queue.

7 Update the UI.

8, 9 For the iPad, set the detail item.

10 On the iPhone, the detail item is set in performSegueWithIdentifier:sender.

11 Re-enable updating (see Note 2 for disabling it).

Whether you have just created a new document or are looking at existing documents, the master view controller is responsible for handling the tap in the list of documents and setting the correct detail item.

This is one difference between the iPad and iPhone versions. On iPad, there is no segue from one view to another: the detail view is always visible. (It’s the master view that is sometimes hidden when the device is rotated to a portrait position.) Thus, on iPad, you handle the tap in a row in the table view by setting the detail item in MasterViewController.m, as you see in Listing 13.15.

Listing 13.15 Responding to a Tap in the Table View


- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([[UIDevice currentDevice]
userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
NSURL * theURL = [_iCloudController documentURLAt: [indexPath row]]; //1
self.detailViewController.detailItem = theURL; //2
self.detailViewController.navigationItem.title =
theCell.textLabel.text; //3
}
}


1 Ask the shared iCloud controller for the object at the table row.

2 Set the detailViewController detailItem to the object. Remember that this statement will invoke the setter for the property. As you’ll see in Listing 13.16, the default setter has been overridden.

3 Set the title of the navigation item.

tableView didSelectRowAtIndexPath: isn’t called when you’re running on iPad if you’re using a storyboard (as this project does). That’s all handled when you set up the standard segue. This means that you set the detail item in prepareForSegue:, as shown in Listing 13.16.

Listing 13.16 Handling the Segue


- (void)prepareForSegue: (UIStoryboardSegue*)segue sender:(id)sender {
NSLog (@" %@ %@", segue.identifier, DisplayDetailSegue);
if ([segue.identifier isEqualToString: DisplayDetailSegue]) { //1

DetailViewController *destVC =
(DetailViewController *)segue.destinationViewController; //2

NSIndexPath *cellPath = [self.tableView indexPathForSelectedRow];
UITableViewCell *theCell = [self.tableView
cellForRowAtIndexPath: cellPath];
NSURL * theURL = [_iCloudController documentURLAt: [cellPath row]];
destVC.detailItem = theURL;
destVC.navigationItem.title = theCell.textLabel.text; //3
}
}


1 Even if you’re certain there’s only one possible segue, check the identifier so that if and when you extend this code, it will work properly.

2 Whether you assume that the destination view controller in the segue is the same as the destination view self.detailViewController is up to you. They should be—and you may even want to test that.

3 Set the title of the navigation item.

Deleting a document is a collaboration between the master view controller and the shared iCloud controller. As in the case of adding a document (see Listing 13.14), you need to delete it from the shared iCloud controller’s list of documents as well as from the user interface. This is done in your override of tableView:commitEditingStyle:forRowAtIndexPath:, as shown in Listing 13.17.

Listing 13.17 Deleting a Document


- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle) editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) { //1
NSURL *fileURL = [_iCloudController documentURLAt: [indexPath row]]; //2

[_iCloudController deleteDocument:fileURL]; //3

[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic]; //4
}
}


1 Check to see if you are deleting a row from the table.

2 Get the document URL for the row from the iCloud controller.

3 Ask the iCloud controller to delete the row.

4 Delete the row from the interface yourself.

DetailViewController

Whether you are arriving at the detail view controller through a navigation interface on iPhone or with a split view controller on iPad, now you have to worry about displaying the data. As noted previously, this is a minimal document that simply contains a text string. The code you need to worry about is simpler than the shared iCloud controller and the master view controller.

DetailViewController.h

First of all you need to update the template for DetailViewController.h, as shown in Listing 13.18.

Listing 13.18 DetailViewController.h


#import <UIKit/UIKit.h>

#import "ReportDocument.h" //1

@interface DetailViewController : UIViewController
<UISplitViewControllerDelegate, ReportDocumentDelegate> //2

@property (weak, nonatomic) IBOutlet UITextView *textView; //3
@property (strong, nonatomic) id detailItem;

@end


1 You’ll need to import your custom document class. If you know you’ll be using this in advance, you can import it now. Often, in the real world, you decide to implement a custom class and come back to add your import statements later on.

2 When you implement the custom document class, it will have a delegate protocol that you’ll need to adopt.

3 The Master-Detail Application template uses a UILabel to display its data. Remove that from the storyboard and add a text view. Make sure to connect it to this property.

DetailViewController.m

In DetailViewController.m, you’ll need to manage setting the detail item (the document), as well as handling notifications when the detail view appears and disappears. Listing 13.19 shows the top of DetailViewController.m with the class extension and the setter for the detail item.

Listing 13.19 Class Extension and Setter for the Detail Item


#import "DetailViewController.h"

@interface DetailViewController ()

@property (strong, nonatomic) UIPopoverController *masterPopoverController;
@property (strong, nonatomic) ReportDocument *document; //1

@end

@implementation DetailViewController

#pragma mark - Managing the detail item

- (void)setDetailItem:(id)newDetailItem //2
{
if (_detailItem != newDetailItem) {
_detailItem = newDetailItem;
_document = [[ReportDocument alloc] initWithFileURL:self.detailItem];//3
_document.delegate = self;

// open or create
NSFileManager *fm = [NSFileManager defaultManager]; //4
if ([fm fileExistsAtPath: [self.detailItem path]])
[_document8:nil];
else
//save
[_document saveToURL:self.detailItem
forSaveOperation:UIDocumentSaveForCreating completionHandler:nil];
}
}


1 This is the document for the custom document type that you’ll use.

2 Override the default setter for the detail item so that you can do additional processing.

3 Create the new document and initialize it with the detail item, which is its URL.

4 Get the default file manager and use it to see if the new document exists. If it does, open it. If it doesn’t, create it.

Listing 13.20 shows you the code to manage the document when the view appears and disappears. Compare the document handling code with Listing 13.19, and you’ll find some duplication. This is because views appearing and disappearing happens on the iPhone in this app. With a split view controller on iPad, the split view appears at the beginning and then remains visible with changing data as needed.

Listing 13.20 Handling Notifications When View Appears and Disappears


- (void)viewWillAppear:(BOOL)animated { //5
[super viewWillAppear:animated];

self.textView.text = @"";

if (self.detailItem) {
_document = [[ReportDocument alloc] initWithFileURL:self.detailItem];
_document.delegate = self;

// open or create
NSFileManager *fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath: [self.detailItem path]])
[_document openWithCompletionHandler:^(BOOL success) { //6
if (!success) {
NSLog(@"ERRROR: cannot open %@", _document.fileURL);
}
}];
else
//save
[_document saveToURL:self.detailItem
forSaveOperation:UIDocumentSaveForCreating completionHandler:nil];
}

// register for keyboard notifications //7
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector
(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector
(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];

}

- (void)viewWillDisappear:(BOOL)animated { //8
[super viewWillDisappear: animated];

NSString *newText = self.textView.text;
_document.documentText = newText;

[_document closeWithCompletionHandler: ^(BOOL success) { //9
if (!success) {
NSLog(@"ERROR: Can't close %@", _document.fileURL);
}
}];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillShowNotification object:nil]; //10
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillHideNotification object:nil];

}


5 You can optimize this code by checking to see if the app is running on an iPad. If so, you can omit the code that’s already been executed in Listing 13.19. On both, you need to register for keyboard show and hide events.

6 You can pass in nil for the completion handler, but, particularly during debugging, it’s often useful to pass in a block that reports an error if it occurs. You can add diagnostics to the message; also, if you set a breakpoint within the completion handler block, you’ll be able to inspect data to help with troubleshooting.

7 Register for keyboard notifications

8 Remember, this will not be executing on iPad.

9 You can use nil for the completion handler, but a block such as this can help with debugging.

10 Remove notification observer.

ReportDocument

The most basic type of document is used in this example: it contains nothing but a text string and its title. As you will see in Chapters 15 and 16, there are two types of documents—NSDocument for OS X and UIDocument for iOS. You will build on this framework in the following chapters. For the purpose of getting started in this chapter, you just need these basics.

As you have seen, much of the communication between threads and with asynchronous processes such as iCloud is facilitated by notifications. This document is no exception. When its text is changed, a notification is posted, and any observers can then update their interface displays.

ReportDocument.h

The brief header to ReportDocument.h is shown in Listing 13.21.

Listing 13.21 ReportDocument.h


#import <UIKit/UIKit.h>

@class ReportDocument; //1

@protocol ReportDocumentDelegate <NSObject> //2
@optional
- (void)documentContentsDidChange: (ReportDocument *)document;
@end

@interface ReportDocument : UIDocument //3

@property (copy, nonatomic) NSString* documentText;
@property (weak, nonatomic) id<ReportDocumentDelegate> delegate;
@end


1 Although this is the header to ReportDocument.h, you need a forward class declaration to the ReportDocument class in order to declare the protocol (see line 2).

2 The ReportDocumentDelegate protocol has one optional method—documentContentsDidChange. An interface class (DetailView-Controller in this app’s case) can adopt the protocol and be notified of changes.

3 This is the basic class declaration. Two properties are declared: one for the document text and the other for the delegate.

ReportDocument.m

There are only three methods in this file; none of them is visible in the header, so they can only be invoked by this class itself. Methods in DetailViewController, which is the delegate for ReportDocument instances, also serve to manipulate the document data.

You’ll notice that undo is implemented (at line 2 in Listing 13.22). That may surprise you because this is a very bare-bones implementation that focuses on iCloud. Undo may seem like an advanced topic for sophisticated users and developers. It isn’t with the current document architecture. A document’s undo manager keeps track of changes when they are done, undone, and redone. It sends notifications to the document itself, which, in turn, updates its change count and arranges to save the document automatically as needed. Without the undo manager, the document won’t know that it has changes that need to be saved.

The code for ReportDocument.m is shown in Listing 13.22.

Listing 13.22 ReportDocument.m


#import "ReportDocument.h"

@interface ReportDocument ()

@end

@implementation ReportDocument

- (void)setDocumentText: (NSString *)newText { //1

NSString *oldText = _documentText;
_documentText = [newText copy];

[self.undoManager registerUndoWithTarget:
selector:@selector (setdocumentText:) object:oldText]; //2

}

- (id)contentsForType: (NSString *)typeName error: (NSError **)outError {//3
if (!self.documentText) self.documentText = @"";
NSData *docData = [self.documentText
dataUsingEncoding: NSUTF8StringEncoding]; //4
return docData;
}

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName
error:(NSError *__autoreleasing *)outError { //5
if ([contents length] > 0)
self.documentText = [[NSString alloc] initWithData: contents
encoding: NSUTF8StringEncoding];
else
self.documentText = @"";


if (self.delegate && [self.delegate //6
respondsToSelector: @selector(documentContentsDidChange:)])
self.delegate documentContentsDidChange: self];

return YES;
}

@end


1 This method, which is usually called from an interface class or from loadFromContents:ofType:error (line 3), sets the text from a string.

2 Set up the undo manager to undo setDocumentText:.

3 contentsForType:error is a standard UIDocument class that is used to load data.

4 Notice in the code here that the string that is stored is converted to an NSData object.

5 This is the basic UIDocument code to load a document’s content.

6 This is standard code to first check if there is a delegate, and then, if so, check whether the delegate responds to the signature.

Storyboards

You might need to make a few changes to storyboards. The code is described in this chapter, but here is a summary of the storyboard changes to support it:

Image addDocument needs to be added. Use the Assistant editor to control-drag from the + in the master view controller to MasterViewController.h. A blank definition will be created in MasterViewController.m, and you can add the code there.

Image The segue from the master view controller to the detail view controller in the iPhone storyboard may need to be renamed to DisplayDetailSegue to match the code here.

Image On both storyboards, remove the UILabel detail item and replace it with a text view, as described in the DetailViewController section.


Note

If you have any warnings about certificates or provisioning profiles, handle them now.


Chapter Summary

This chapter gave you a basic structure for managing iCloud documents on iOS. It built on and extended Apple’s “Third iOS App: iCloud.” In this version, you have a structure that combines the Master-Detail Application template with a new class—SharediCloudController—that manages iCloud interactions. The controller not only manages the basic interface with iCloud but also manages the list of app documents that are stored on iCloud. You saw how adding and deleting documents requires them to be added and deleted from the user interface (mostly in the master view controller) as well as from the list of documents maintained by the controller.

Exercises

1. If you have followed along with the app, build and run it. Use developer.icloud.com to inspect your iCloud storage as you add and delete documents. Experiment with downloading documents from developer.icloud.com to check their contents with what you can examine with the Xcode debugger and breakpoints.

2. Make a copy of the app and modify it so that it includes features from projects you’re working on. In other words, build your app on top of the iCloud app from this chapter. As soon as it’s your app and your app’s language, it will look very different.

3. If you have two iOS devices, try to break the app. Using Airplane mode and Settings, turn iCloud on and off and try to get the two devices to create duplicate document names. Get a feel for how long the documents list takes to rebuild when you turn a device on with Airplane mode.