Documents and Data Sharing - The Core iOS Developer’s Cookbook, Fifth Edition (2014)

The Core iOS Developer’s Cookbook, Fifth Edition (2014)

Chapter 11. Documents and Data Sharing

Under iOS, applications can share information and data as well as move control from one application to another, using a variety of system features. Each application has access to a common system pasteboard that enables copying and pasting across apps. The apps can request a number of system-supplied “actions” to apply to a document, such as printing, tweeting, or posting to Facebook. Apps can declare custom URL schemes that can be embedded in text and web pages. This chapter introduces the ways you can integrate documents and data sharing between applications. You’ll see how to add these features to your applications and use them smartly to make your app a cooperative citizen of the iOS ecosystem.

Recipe: Working with Uniform Type Identifiers

Uniform Type Identifiers (UTIs) represent a central component of iOS information sharing. You can think of them as the next generation of MIME types. UTIs are strings that identify resource types such as images and text. UTIs specify what kind of information is being used for common data objects. They do this without relying on older indicators, such as file extensions, MIME types, or file-type metadata such as OSTypes. UTIs replace these items with a newer and more flexible technology.

UTIs use a reverse-domain-style naming convention. Common Apple-derived identifiers look like this: public.html and public.jpeg. These refer, respectively, to HTML source text and JPEG images, which are both specialized types of information.

Inheritance plays an important role with UTIs. UTIs use an OO-like system of inheritance, where child UTIs have an “is-a” relationship to parents. Children inherit all attributes of their parents but add further specificity of the kind of data they represent. That’s because each UTI can assume a more general or more specific role, as needed. Take the JPEG UTI, for example. A JPEG image (public.jpeg) is an image (public.image), which is in turn a kind of data (public.data), which is a kind of user-viewable (or listenable) content (public.content), which is a kind of item (public.item), the generic base type for UTIs. This hierarchy is called conformance, where child UTIs conform to parent UTIs. For example, the more specific jpeg UTI conforms to the more general image or data UTI.

Figure 11-1 shows part of Apple’s basic conformance tree. Any item lower down on the tree must conform to all of its parent data attributes. Declaring a parent UTI implies that you support all of its children. So, an application that can open public.data must service text, movies, image files, and more.

Image

Figure 11-1 Apple’s public UTI conformance tree.

UTIs enable multiple inheritance. An item can conform to more than one UTI parent. So, you might imagine a data type that offers both text and image containers, which declares conformance to both.

There is no central registry for UTI items, although each UTI should adhere to conventions. The public domain is reserved for iOS-specific types, common to most applications. Apple has generated a complete family hierarchy of public items. Add any third-party company-specific names by using standard reverse domain naming (for example, com.sadun.myCustomType and com.apple.quicktime-movie).

Determining UTIs from File Extensions

The Mobile Core Services module offers utilities that enable you to retrieve UTI information based on file extensions. Be sure to import the module when using these C-based functions. The following function returns a preferred UTI when passed a path extension string. The preferred identifier is a single UTI string:

@import MobileCoreServices;

NSString *preferredUTIForExtension(NSString *ext)
{
// Request the UTI for the file extension
NSString *theUTI = (__bridge_transfer NSString *)
UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
(__bridge CFStringRef) ext, NULL);
return theUTI;
}

You can pass a MIME type instead of a file extension to UTTypeCreatePreferredIdentifier-ForTag() by using kUTTagClassMIMEType as the first argument. This function returns a preferred UTI for a given MIME type:

NSString *preferredUTIForMIMEType(NSString *mime)
{
// Request the UTI for the MIME type
NSString *theUTI = (__bridge_transfer NSString *)
UTTypeCreatePreferredIdentifierForTag(
kUTTagClassMIMEType,
(__bridge CFStringRef) mime, NULL);
return theUTI;
}

Together these functions enable you to move from file extensions and MIME types to the UTI types used for modern file access.

Moving from UTI to Extension or MIME Type

To go the other way, producing a preferred extension or MIME types from a UTI, use UTTypeCopyPreferredTagWithClass(). The following functions return jpeg and image/jpeg, respectively, when passed public.jpeg:

NSString *extensionForUTI(NSString *aUTI)
{
CFStringRef theUTI = (__bridge CFStringRef) aUTI;
CFStringRef results =
UTTypeCopyPreferredTagWithClass(
theUTI, kUTTagClassFilenameExtension);
return (__bridge_transfer NSString *)results;
}

NSString *mimeTypeForUTI(NSString *aUTI)
{
CFStringRef theUTI = (__bridge CFStringRef) aUTI;
CFStringRef results =
UTTypeCopyPreferredTagWithClass(
theUTI, kUTTagClassMIMEType);
return (__bridge_transfer NSString *)results;
}

You must work at the leaf level with these functions—at the level that declares the type extensions directly. You cannot reference the parent types. Extensions are declared in property lists, where features like file extensions and default icons are described. So, for example, passingpublic.text or public.movie to the extension function returns nil, whereas public.plain-text and public.mpeg return extensions of txt and mpg, respectively.

The former items live too high up the conformance tree, providing an abstract type rather than a specific implementation. There’s no current API function to look down to find items that descend from a given class that are currently defined for the application. You may want to file an enhancement request at bugreport.apple.com. Surely, all the extensions and MIME types are registered somewhere (otherwise, how would the UTTypeCopyPreferredTagWithClass() lookup work in the first place?), so the ability to map extensions to more general UTIs should be possible.

MIME Helper

Although the extension-to-UTI service is exhaustive, returning UTIs for nearly any extension you throw at it, the UTI-to-MIME results are scattershot. You can usually generate a proper MIME representation for any common item; less common ones are rare.

The following lines show an assortment of extensions, their UTIs (retrieved via preferredUTIForExtension()), and the MIME types generated from each UTI (via mimeTypeForUTI()):

xlv: dyn.age81u5d0 / (null)
xlw: com.microsoft.excel.xlw / application/vnd.ms-excel
xm: dyn.age81u5k / (null)
xml: public.xml / application/xml
z: public.z-archive / application/x-compress
zip: public.zip-archive / application/zip
zoo: dyn.age81y55t / (null)
zsh: public.zsh-script / (null)

As you can see, there are quite a number of blanks. These functions return nil when they cannot find a match. To address this problem, the sample code for this recipe includes an extra MIMEHelper class. It defines one function, which returns a MIME type for a supplied extension:

NSString *mimeForExtension(NSString *extension);

Its extensions and MIME types are sourced from the Apache Software Foundation, which has placed its list in the public domain. Out of the 450 extensions in the sample code for this recipe, iOS returned all 450 UTIs but only 89 MIME types. The Apache list ups this number to 230 recognizable MIME types.

Testing Conformance

You test conformance using the UTTypeConformsTo() function. This function takes two arguments: a source UTI and a UTI to compare to. It returns true if the first UTI conforms to the second. Use this to test whether a more specific item conforms to a more general one. Test equality using UTTypeEqual(). Here’s an example of how you might use conformance testing to determine whether a file path likely points to an image resource:

BOOL pathPointsToLikelyUTIMatch(NSString *path, CFStringRef theUTI)
{
NSString *extension = [path pathExtension];
NSString *preferredUTI = preferredUTIForExtension(extension);
return (UTTypeConformsTo(
(__bridge CFStringRef) preferredUTI, theUTI));
}

BOOL pathPointsToLikelyImage(NSString *path)
{
return pathPointsToLikelyUTIMatch(path, CFSTR("public.image"));
}

BOOL pathPointsToLikelyAudio(NSString *path)
{
return pathPointsToLikelyUTIMatch(path, CFSTR("public.audio"));
}

Retrieving Conformance Lists

UTTypeCopyDeclaration() offers the most general (and most useful) of all UTI functions in the iOS API. It returns a dictionary that includes the following keys:

Image kUTTypeIdentifierKey—The UTI name, which you passed to the function (for example, public.mpeg)

Image kUTTypeConformsToKey—Any parents that the type conforms to (for example, public.mpeg conforms to public.movie)

Image kUTTypeDescriptionKey—A real-world description of the type in question, if one exists (for example, “MPEG movie”)

Image kUTTypeTagSpecificationKey—A dictionary of equivalent OSTypes (for example, MPG and MPEG), file extensions (mpg, mpeg, mpe, m75, and m15), and MIME types (video/mpeg, video/mpg, video/x-mpeg, and video/x-mpg) for the given UTI

In addition to these common items, you encounter more keys that specify imported and exported UTI declarations (kUTImportedTypeDeclarationsKey and kUTExportedType-DeclarationsKey), icon resources to associate with the UTI (kUTTypeIconFileKey), a URL that points to a page describing the type (kUTTypeReferenceURLKey), and a version key that offers a version string for the UTI (kUTTypeVersionKey).

Use the returned dictionary to ascend through the conformance tree to build an array that represents all the items that a given UTI conforms to. For example, the public.mpeg type conforms to public.movie, public.audiovisual-content, public.data, public.item, and public.content. These items are returned as an array from the conformanceArray function in Recipe 11-1.

Recipe 11-1 Testing Conformance


// Build a declaration dictionary for the given type
NSDictionary *utiDictionary(NSString *aUTI)
{
NSDictionary *dictionary =
(__bridge_transfer NSDictionary *)
UTTypeCopyDeclaration((__bridge CFStringRef) aUTI);
return dictionary;
}

// Return an array where each member is guaranteed unique
// but that preserves the original ordering wherever possible
NSArray *uniqueArray(NSArray *anArray)
{
NSMutableArray *copiedArray =
[NSMutableArray arrayWithArray:anArray];

for (id object in anArray)
{
[copiedArray removeObjectIdenticalTo:object];
[copiedArray addObject:object];
}

return copiedArray;
}

// Return an array representing all UTIs that a given UTI conforms to
NSArray *conformanceArray(NSString *aUTI)
{
NSMutableArray *results =
[NSMutableArray arrayWithObject:aUTI];
NSDictionary *dictionary = utiDictionary(aUTI);
id conforms = [dictionary objectForKey:
(__bridge NSString *)kUTTypeConformsToKey];

// No conformance
if (!conforms) return results;

// Single conformance
if ([conforms isKindOfClass:[NSString class]])
{
[results addObjectsFromArray:conformanceArray(conforms)];
return uniqueArray(results);
}

// Iterate through multiple conformance
if ([conforms isKindOfClass:[NSArray class]])
{
for (NSString *eachUTI in (NSArray *) conforms)
[results addObjectsFromArray:conformanceArray(eachUTI)];
return uniqueArray(results);
}

// Just return the one-item array
return results;
}



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 11.


Recipe: Accessing the System Pasteboard

Pasteboards, also known as clipboards on some systems, provide a central OS feature for sharing data across applications. Users can copy data to the pasteboard in one application, switch tasks, and then paste that data into another application. Cut/copy/paste features are similar to those found in most other operating systems. Users can also copy and paste within a single application, when switching between text fields or views, and developers can establish private pasteboards for app-specific data that other apps would not understand.

The UIPasteboard class offers access to a shared device pasteboard and its contents. This snippet returns the general system pasteboard, which is appropriate for most general copy/paste use:

UIPasteboard *pb = [UIPasteboard generalPasteboard];

The system-provided general pasteboard and the find pasteboards are shared across all applications on the device. In addition to the shared system pasteboards, iOS offers both application-specific and custom-named pasteboards that can be used across applications from the same organization with a common team ID in the application portal. Create app-specific pasteboards using pasteboardWithUniqueName, which returns an application pasteboard object that persists until the application quits.

Create shared pasteboards using pasteboardWithName:create:, which returns a pasteboard with the specified name. The create parameter specifies whether the system should create the pasteboard if it does not yet exist. This kind of pasteboard can persist beyond a single application run; set the persistent property to YES after creation. Use removePasteboardWithName: to destroy a pasteboard and free up the resources it uses.


Note

Prior to iOS 7, custom-named pasteboards could be shared across all applications aware of the pasteboard name, not just applications from the same organization and application group. This has changed in iOS 7, as described in the iOS 7 Release Notes. This change breaks numerous existing applications that relied on publicly sharable custom pasteboards. You now need new methods for sharing between apps. Consider using openURL (see Recipe 11-8) or external shared storage.


Storing Data

A pasteboard can store one or more items at a time. Each pasteboard item is represented as a dictionary containing one or more key-value pairs that store the data and the associated type. A single pasteboard item might contain multiple entries to make it more likely that other apps can find a compatible data type. A UTI is commonly used to specify what kind of data is stored. For example, you might find public.text (and, more specifically, public.utf8-plain-text) to store text data, public.url for URL address, and public.jpeg for image data. These are among the many common data types used on iOS.

UIPasteboard provides methods to work with a single pasteboard item or multiple pasteboard items at a time, including investigating the data types of items as well as getting and setting pasteboard data. Many of the single pasteboard item methods work specifically on the first item in the pasteboard. You can retrieve an array of all available items via the pasteboard’s items property.

You can set the data and associate a type for the first item in the pasteboard by passing an NSData object and a UTI that describes a type the data conforms to:

[[UIPasteboard generalPasteboard]
setData:theData forPasteboardType:theUTI];

Alternatively, for property list objects (that is, string, date, array, dictionary, number, or URL), set an NSValue via setValue:forPasteboardType:. These property list objects are stored internally somewhat differently than their raw-data cousins, giving rise to the method differentiation.

Storing Common Types

Pasteboards are further specialized for several data types, which represent the most commonly used pasteboard items. These are colors (not a property list “value” object), images (also not a property list “value” object), strings, and URLs. The UIPasteboard class provides specialized getters and setters to make it easier to handle these items. You can treat each of these as properties of the pasteboard, so you can set and retrieve them using dot notation. What’s more, each property has a plural form, allowing you to access those items as arrays of objects.

Pasteboard properties greatly simplify using the system pasteboard for the most common use cases. The property accessors include the following:

Image string—Sets or retrieves the string of the first pasteboard item

Image strings—Sets or retrieves an array of all strings on the pasteboard

Image image—Sets or retrieves the image of the first pasteboard item

Image images—Sets or retrieves an array of all images on the pasteboard

Image URL—Sets or retrieves the URL of the first pasteboard item

Image URLs—Sets or retrieves an array of all URLs on the pasteboard

Image color—Sets or retrieves the first color on the pasteboard

Image colors—Sets or retrieves an array of all colors on the pasteboard

Retrieving Data

When using one of the four special classes listed previously, simply use the associated property to retrieve data from the pasteboard. Otherwise, you can fetch data using the dataForPasteboardType: method. This method returns the data from the first item in the pasteboard. Any other items in the pasteboard are ignored.

If you need to retrieve all matching data, recover an itemSetWithPasteboardTypes: and then iterate through the set to retrieve each dictionary. Recover the data type for each item from the single dictionary key and the data from its value.

Modified pasteboards issue a UIPasteboardChangedNotification, which you can listen to via a default NSNotificationCenter observer. You can also watch custom pasteboards and listen for their removal via UIPasteboardRemovedNotification.


Note

If you want to successfully paste text data to Notes or Mail, use public.utf8-plain-text as your UTI of choice when storing information to the pasteboard. Using the string or strings properties automatically enforces this UTI.


Passively Updating the Pasteboard

iOS’s selection and copy interfaces are not, frankly, the most streamlined elements of the operating system. There are times when you want to simplify matters for your user while preparing content that’s meant to be shared with other applications.

Consider Recipe 11-2. It enables the user to use a text view to enter and edit text, while automating the process of updating the pasteboard. When the watcher is active (toggled by a simple button tap), the text updates the pasteboard on each edit. This is accomplished by implementing a text view delegate method (textViewDidChange:) that responds to edits by automatically assigning changes to the pasteboard (updatePasteboard).

This recipe demonstrates the relative simplicity involved in accessing and updating the pasteboard.

Recipe 11-2 Automatically Copying Text to the Pasteboard


- (void)updatePasteboard
{
// Copy the text to the pasteboard when the watcher is enabled
if (enableWatcher)
[UIPasteboard generalPasteboard].string = textView.text;
}

- (void)textViewDidChange:(UITextView *)textView
{
// Delegate method calls for an update
[self updatePasteboard];
}

- (void)toggle:(UIBarButtonItem *)bbi
{
// switch between standard and auto-copy modes
enableWatcher = !enableWatcher;
bbi.title = enableWatcher ? @"Stop Watching" : @"Watch";
}



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 11.


Recipe: Monitoring the Documents Folder

iOS documents aren’t trapped in their sandboxes. You can and should share them with your users. Offer users direct control over their documents and access to any material they may have created on-device. A simple Info.plist setting enables iTunes to display the contents of a user’s Documents folder and enables those users to add and remove material on demand.

At some point in the future, you may use a simple NSMetadataQuery monitor to watch your Documents folder and report updates. At this writing, that metadata surveillance is not yet extended beyond iCloud for use with other folders. Code ported from OS X fails to work as expected on iOS. At this writing, there are precisely two available search domains for iOS: the ubiquitous data scope and the ubiquitous documents scope (that is, iCloud and iCloud). Until general functionality arrives in iOS, use kqueue. This older technology provides scalable event notification. Withkqueue, you can monitor, add, and clear events. This roughly equates to looking for files being added and deleted, which are the primary kinds of updates you want to react to. Recipe 11-3 presents a kqueue implementation for watching the Documents folder.

Enabling Document File Sharing

To enable file sharing, add an Application Supports iTunes File Sharing key to the application’s Info.plist and set its value to YES. You can edit the plist directly or use the Xcode-provided editor. The editor is accessible in the Custom iOS Target Properties of the application target in the Project > Target > Info screen, as shown in Figure 11-2. When working with raw keys and values, this item is called UIFileSharingEnabled. iTunes lists all applications that declare file-sharing support in each device’s Apps tab, as shown in Figure 11-3.

Image

Figure 11-2 Enable Application Supports iTunes File Sharing to allow user access to the Documents folder via iTunes.

Image

Figure 11-3 Each installed application that declares UIFileSharingEnabled is listed in iTunes in the device’s Apps tab.

User Control

You cannot specify which kinds of items are allowed to be in the Documents folder. Users can add any materials they like, and they can remove any items they want to remove. What they cannot do, however, is navigate through subfolders using the iTunes interface. Notice the Inbox folder inFigure 11-3. This is an artifact left over from application-to-application document sharing, and it should not be there. Users cannot manage that data directly, and you should not leave the subfolder there to confuse them.

Users cannot delete the Inbox in iTunes the way they can delete other files and folders. Nor should your application write files directly to the Inbox. Respect the Inbox’s role, which is to capture any incoming data from other applications. When you implement file-sharing support, always check for an Inbox on resuming active status and process that data to clear out the Inbox and remove it whenever your app launches and resumes. Best practices for handling incoming documents are discussed later in this chapter.

Xcode Access

As a developer, you have access not only to the Documents folder but also to the entire application sandbox. Use the Xcode Organizer (Command-Shift-2) > Devices tab > Device > Applications > Application Name to browse, upload, and download files to and from the sandbox.

Test basic file sharing by enabling the UIFileSharingEnabled property to an application and loading data to your Documents folder. After those files are created, use Xcode and iTunes to inspect, download, and delete them.

Scanning for New Documents

Recipe 11-3 works by requesting kqueue notifications in its beginGeneratingDocument-NotificationsInPath: method. Here, it retrieves a file descriptor for the path you supply (in this case, the Documents folder) and requests notifications for add and clear events. It adds this functionality to the current run loop, enabling notifications whenever the monitored folder updates.

Upon receiving that callback, it posts a notification (for example, the custom kDocument-Changed, in the kqueueFired method) and continues watching for new events. This all runs in the primary run loop on the main thread, so the GUI can respond and update itself upon receiving the notification.

The following snippet demonstrates how you might use Recipe 11-3’s watcher to update a file list in your GUI. Whenever the contents change, an update notification allows the app to refresh those directory contents listings:

- (void)scanDocuments
{
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"Documents"];
items = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:path error:nil];
[self.tableView reloadData];
}

- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[UITableViewCell class]
forCellReuseIdentifier:@"cell"];
[self scanDocuments];

// React to content changes
[[NSNotificationCenter defaultCenter]
addObserverForName:kDocumentChanged
object:nil queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *notification){
[self scanDocuments];
}];

// Start the watcher
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"Documents"];
helper = [DocWatchHelper watcherForPath:path];
}

Test this recipe by connecting a device to iTunes. Add and remove items using the iTunes App tab interface. The device’s onboard file list updates to reflect those changes in real time.

There are some cautions to be aware of when using Recipe 11-3. First, for larger documents, you shouldn’t be reading the file immediately after you’re notified of their creation. You might want to poll file sizes to determine when data has stopped being written. Second, iTunes File Sharing transfer can, upon occasion, stall. Code accordingly.

Recipe 11-3 Using a kqueue File Monitor


#import <fcntl.h>
#import <sys/event.h>

#define kDocumentChanged \
@"DocumentsFolderContentsDidChangeNotification"

@interface DocWatchHelper : NSObject
@property (strong) NSString *path;
+ (id)watcherForPath:(NSString *)aPath;
@end

@implementation DocWatchHelper
{
CFFileDescriptorRef kqref;
CFRunLoopSourceRef rls;
}

- (void)kqueueFired
{
int kq;
struct kevent event;
struct timespec timeout = { 0, 0 };
int eventCount;

kq = CFFileDescriptorGetNativeDescriptor(self->kqref);
assert(kq >= 0);

eventCount = kevent(kq, NULL, 0, &event, 1, &timeout);
assert( (eventCount >= 0) && (eventCount < 2) );

if (eventCount == 1)
[[NSNotificationCenter defaultCenter]
postNotificationName:kDocumentChanged
object:self];

CFFileDescriptorEnableCallBacks(self->kqref,
kCFFileDescriptorReadCallBack);
}

static void KQCallback(CFFileDescriptorRef kqRef,
CFOptionFlags callBackTypes, void *info)
{
DocWatchHelper *helper =
(DocWatchHelper *)(__bridge id)(CFTypeRef) info;
[helper kqueueFired];
}

- (void)beginGeneratingDocumentNotificationsInPath:
(NSString *)docPath
{
int dirFD;
int kq;
int retVal;
struct kevent eventToAdd;
CFFileDescriptorContext context =
{ 0, (void *)(__bridge CFTypeRef) self,
NULL, NULL, NULL };

dirFD = open([docPath fileSystemRepresentation], O_EVTONLY);
assert(dirFD >= 0);

kq = kqueue();
assert(kq >= 0);

eventToAdd.ident = dirFD;
eventToAdd.filter = EVFILT_VNODE;
eventToAdd.flags = EV_ADD | EV_CLEAR;
eventToAdd.fflags = NOTE_WRITE;
eventToAdd.data = 0;
eventToAdd.udata = NULL;

retVal = kevent(kq, &eventToAdd, 1, NULL, 0, NULL);
assert(retVal == 0);

self->kqref = CFFileDescriptorCreate(NULL, kq,
true, KQCallback, &context);
rls = CFFileDescriptorCreateRunLoopSource(
NULL, self->kqref, 0);
assert(rls != NULL);

CFRunLoopAddSource(CFRunLoopGetCurrent(), rls,
kCFRunLoopDefaultMode);
CFRelease(rls);

CFFileDescriptorEnableCallBacks(self->kqref,
kCFFileDescriptorReadCallBack);
}

- (void)dealloc
{
self.path = nil;
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), rls,
kCFRunLoopDefaultMode);
CFFileDescriptorDisableCallBacks(self->kqref,
kCFFileDescriptorReadCallBack);
}

+ (id)watcherForPath:(NSString *)aPath
{
DocWatchHelper *watcher = [[self alloc] init];
watcher.path = aPath;
[watcher beginGeneratingDocumentNotificationsInPath:aPath];
return watcher;
}
@end



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 11.


Recipe: Activity View Controller

Introduced in iOS 6, the activity view controller integrates data activities into the interface shown in Figure 11-4. With minimal development cost on your part, this controller enables your users to copy items to the pasteboard, post to social media, share via e-mail and texting, and more. Built-in activities include Facebook, Twitter, Weibo, SMS, mail, printing, copying to pasteboard, assigning data to a contact, and saving to the Camera Roll. iOS 7 adds a new set of activities, including adding to the Reading List, Flickr, Vimeo, Weibo, and AirDrop. Apps can define their own custom services as well, which you’ll read about later in this section. The comprehensive list of current activity types includes the following:

Image UIActivityTypePostToFacebook

Image UIActivityTypePostToTwitter

Image UIActivityTypePostToWeibo

Image UIActivityTypeMessage

Image UIActivityTypeMail

Image UIActivityTypePrint

Image UIActivityTypeCopyToPasteboard

Image UIActivityTypeAssignToContact

Image UIActivityTypeSaveToCameraRoll

Image UIActivityTypeAddToReadingList

Image UIActivityTypePostToFlickr

Image UIActivityTypePostToVimeo

Image UIActivityTypePostToTencentWeibo

Image UIActivityTypeAirDrop

Image

Figure 11-4 The UIActivityViewController class offers system and custom services.

Significantly missing from this list are two important activities: Open in... for sharing documents between applications and Quick Look for previewing files. These two features are discussed later in this chapter, with recipes that show you how to support these features independently and, in the case of Quick Look, integrated with the activity view controller.

Presenting the Activity View Controller

How you present the controller varies by device. Show it modally on members of the iPhone family and in a popover on tablets. The UIBarButtonSystemItemAction icon provides the perfect way to populate bar buttons linking to this controller.

Best of all, almost no work is required on your end. After users select an activity, the controller handles all further interaction, such as presenting a mail or Twitter composition sheet, adding a picture to the onboard library, or assigning it to a contact.

Activity Item Sources

Recipe 11-4 creates and presents the activity view controller from code. This implementation has its main class adopt the UIActivityItemSource protocol and adds self to the items array passed to the controller:

UIActivityViewController *activity =
[[UIActivityViewController alloc] initWithActivityItems:@[self]
applicationActivities:nil];
[self presentViewController:activity];

The UIActivityItemSource—self in this case—represents the data that is being acted upon.

The protocol’s two mandatory methods supply the item to process (the data that will be used for the activity) and a placeholder for that item. The item corresponds to an object that’s appropriate for a given activity type. You can vary which item you return based on the kind of activity that’s passed to the callback. For example, you might tweet “I created a great song in App Name,” but you might send the actual sound file through e-mail.

The placeholder for an item is typically the same data returned as the item unless you have objects that you must process or create. In that case, you can create a placeholder object without real data.

Both callbacks run on the main thread, so keep your data small. If you need to process your data, consider using a provider described in the next section instead.

Optional methods introduced in iOS 7 allow the delegate to configure further options on your data. Delegate methods are provided to return the thumbnail preview image, subject text, and a UTI for the specified activity type. These elements can be used by activity services that support them.

Item Providers

Extending the previous approach, the UIActivityItemProvider class conforms to the UIActivityItemSource protocol and enables you to delay passing data. It’s a type of operation (NSOperation) that offers you the flexibility to manipulate data before sharing. For example, you might need to process a large video file before it can be uploaded to a social sharing site, or you might need to subsample some audio from a larger sequence.

Subclass the provider class and implement the item method. This takes the place of the main method you normally use with operations. Generate the processed data, safe in the knowledge that the method will run asynchronously without blocking your user’s interactive experience.

Item Source Callbacks

Recipe 11-4 passes self to the controller as part of its items array. self adopts the source protocol (<UIActivityItemSource>), so the controller understands to use callbacks when retrieving data items. The callback methods enable you to vary your data based on each one’s intended use. Use the activity types (such as Facebook or Add to Contacts; they’re listed earlier in this section) to choose the exact data you want to provide. This is especially important when selecting from resolutions for various uses. When printing, keep your data quality high. When tweeting, a low-res image may do the job instead.

If your data is invariant—that is, you’ll be passing the same data to e-mail as you would to Facebook—you can directly supply an array of data items (typically strings, images, and URLs) instead of UIActivityItemSource objects. For example, you could create the controller like this, using a single image:

UIActivityViewController *activity = [[UIActivityViewController alloc]
initWithActivityItems:@[imageView.image]
applicationActivities:nil];

This direct approach is far simpler. Your primary class need not declare the item source protocol; you do not need to implement the extra methods. It’s a quick and easy way to manage activities for simple items.

You’re not limited to passing single items, either. Include additional elements in the activity items array as needed. The following controller might add its two images to an e-mail or save both to the system Camera Roll, depending on the user’s selection:

UIImage *secondImage = [UIImage imageNamed:@"Default.png"];
UIActivityViewController *activity = [[UIActivityViewController alloc]
initWithActivityItems:@[imageView.image, secondImage]
applicationActivities:nil];

Broadening activities to use multiple items enables users to be more efficient while using your app.

Recipe 11-4 The Activity View Controller


- (void)presentViewController:
(UIViewController *)viewControllerToPresent
{
if (popover) [popover dismissPopoverAnimated:NO];
if (IS_IPHONE)
{
[self presentViewController:viewControllerToPresent
animated:YES completion:nil];
}
else
{
popover = [[UIPopoverController alloc]
initWithContentViewController:viewControllerToPresent];
popover.delegate = self;
[popover presentPopoverFromBarButtonItem:
self.navigationItem.leftBarButtonItem
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}
}

// Popover was dismissed
- (void)popoverControllerDidDismissPopover:
(UIPopoverController *)aPopoverController
{
popover = nil;
}

// Return the item to process
- (id)activityViewController:
(UIActivityViewController *)activityViewController
itemForActivityType:(NSString *)activityType
{
return imageView.image;
}

// Return a thumbnail version of that item
- (id)activityViewControllerPlaceholderItem:
(UIActivityViewController *)activityViewController
{
return imageView.image;
}

// Create and present the view controller
- (void)action
{
UIActivityViewController *activity =
[[UIActivityViewController alloc]
initWithActivityItems:@[self]
applicationActivities:nil];
[self presentViewController:activity];
}



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 11.


Adding Services

Each app can provide application-specific services by subclassing the UIActivity class and passing that activity to the UIActivityController on initialization. These custom activities are available in the presented activity view controller alongside the system-provided activities.When selected, the custom activity presents a view controller allowing the user to interact with the passed data or service in some way, such as entering credentials or manipulating the data.

Listing 11-1 introduces a skeletal UIActivity subclass that presents a simple text view. This custom activity is shown in Figure 11-5 as the List Items (Cookbook) option. When this icon is tapped, a custom view controller is presented, displaying a view that lists the items passed to it by the activity controller. It displays each item’s class and description. The view controller includes a handler that updates the calling UIActivity instance by sending activityDidFinish: when the user taps Done.

Image

Figure 11-5 Adding your own custom application activities.

Adding a way for your activity to complete is important, especially when your controller doesn’t have a natural ending point. When your action uploads data to an FTP server, you know when it completes. If it tweets, you know when the status posts. In this example, it’s up to the user to determine when this activity finishes. Make sure your view controller contains a weak property pointing back to the activity so that you can send the did-finish method after your work concludes.

The activity class contains a number of mandatory and optional items. You should implement all the methods shown in the following list. The methods to support a custom activity include the following:

Image activityType—Returns a unique string that describes the type of activity. One of this string’s counterparts in the system-supplied activities is UIActivityTypePostToFacebook. Use a similar naming scheme. This string identifies a particular activity type and what it does.Listing 11-1 returns @"CustomActivityTypeListItemsAndTypes", which describes the activity.

Image activityTitle—You supply the text you want to show in the activity controller. The custom text in Figure 11-5 was returned by this method. Use active descriptions when describing your custom action. Follow Apple’s lead and use, for example, Save to Camera Roll, Print, and Copy. Your title should finish the phrase “I Want to...”—for example, “I Want to Print,” “I Want to Copy,” or, in this example, “I Want to List Items.” Use header case and capitalize each word except for minor ones like to or and.

Image activityImage—Returns an image for the controller to use. The controller converts your image to a one-value bitmap. Use simple art on a transparent background to build the contents of your icon image.

Image canPerformWithActivityItems:—Scans the passed items and decides whether your controller can process them. If so, returns YES.

Image prepareWithActivityItems:—Stores the passed items for later use (here, the passed activity items are assigned to a local instance variable) and performs any necessary preprocessing.

Image activityViewController—Returns a fully initialized presentable view controller, using the activity items passed earlier. This controller is automatically presented to the user, and he or she can customize options before performing the promised action.

Adding custom activities allows your app to expand its data-handling possibilities while integrating features into a consistent system-supplied interface. It’s a powerful iOS feature. The strongest activity choices integrate with system services (such as copying to the pasteboard or saving to the photo album) or provide a connection to off-device APIs, such as Facebook, Twitter, Dropbox, and FTP.

This example, which simply lists items, represents a weak use case. There’s no reason the same feature couldn’t be provided as a normal in-app screen. When you think actions, try to project outside the app. Connect your user’s data with sharing and processing features that expand beyond the normal GUI.

Listing 11-1 Application Activities


// All activities present a view controller. This custom controller
// provides a full-sized text view.
@interface TextViewController : UIViewController
@property (nonatomic, readonly) UITextView *textView;
@property (nonatomic, weak) UIActivity *activity;
@end

@implementation TextViewController

// Make sure you provide a done handler of some kind, such as this
// or an integrated button that finishes and wraps up
- (void)done
{
[_activity activityDidFinish:YES];
}

// Just a super-basic text view controller
- (instancetype)init
{
self = [super init];
if (self)
{
_textView = [[UITextView alloc] init];
_textView.font =
[UIFont fontWithName:@"Futura" size:16.0f];
_textView.editable = NO;

[self.view addSubview:_textView];
PREPCONSTRAINTS(_textView);
STRETCH_VIEW(self.view, _textView);

// Prepare a Done button
self.navigationItem.rightBarButtonItem =
BARBUTTON(@"Done", @selector(done));
}
return self;
}
@end

// A custom activity subclass to display a list of source items
@interface MyActivity : UIActivity
@end

@implementation MyActivity
{
NSArray *items;
}

// A unique type name
- (NSString *)activityType
{
return @"CustomActivityTypeListItemsAndTypes";
}

// The title listed on the controller
- (NSString *)activityTitle
{
return @"List Items (Cookbook)";
}

// A custom image that says "iOS" with a rounded rect edge
- (UIImage *)activityImage
{
CGRect rect = CGRectMake(0.0f, 0.0f, 75.0f, 75.0f);
UIGraphicsBeginImageContext(rect.size);
rect = CGRectInset(rect, 15.0f, 15.0f);
UIBezierPath *path = [UIBezierPath
bezierPathWithRoundedRect:rect cornerRadius:4.0f];
[path stroke];
rect = CGRectInset(rect, 0.0f, 10.0f);
NSMutableParagraphStyle * paragraphStyle =
[[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = NSTextAlignmentCenter;
NSDictionary * attributes =
@{NSParagraphStyleAttributeName : paragraphStyle,
NSFontAttributeName : [UIFont fontWithName:@"Futura"
size:18.0f]};
[@"iOS" drawInRect:rect withAttributes:attributes];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return image;
}

// Specify if you can respond to these items
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems
{
return YES;
}

// Store the items locally for later use
- (void)prepareWithActivityItems:(NSArray *)activityItems
{
items = activityItems;
}

// Return a view controller, in this case one that lists
// its items and their classes
- (UIViewController *)activityViewController
{
TextViewController *tvc = [[TextViewController alloc] init];
tvc.activity = self;
UITextView *textView = tvc.textView;

NSMutableString *string = [NSMutableString string];
for (id item in items)
[string appendFormat:
@"%@: %@\n", [item class], [item description]];
textView.text = string;

// Make sure to provide some kind of done: handler in
// your main controller.
UINavigationController *nav = [[UINavigationController alloc]
initWithRootViewController:tvc];
return nav;
}
@end


Items and Activities

The activities presented for each item vary by the kind of data you pass. Table 11-1 lists offered activities by source data type on a U.S. phone:

Image

Table 11-1 Activity Types for Data Types

These activities may vary based on locale. As you see in the recipes that follow, preview controller support expands beyond these foundation types:

Image iOS’s Quick Look framework integrates activity controllers into its file previews. The Quick Look–provided activity controller can print and e-mail many kinds of documents. Some document types support other activities as well.

Image Document interaction controllers offer “Open in...” features that enable you to share files between applications. The controller adds activities into its “options”-style presentation, combining activities with “Open in...” choices.

Excluding Activities

You can specifically exclude activities by supplying a list of activity types to the excludedActivityTypes property:

UIActivityViewController *activity =
[[UIActivityViewController alloc]
initWithActivityItems:items
applicationActivities:@[appActivity]];
activity.excludedActivityTypes = @[UIActivityTypeMail];

Recipe: The Quick Look Preview Controller

The Quick Look preview controller class enables users to preview many document types. This controller supports text, images, PDF, RTF, iWork files, Microsoft Office documents (Office 97 and later, including DOC, PPT, XLS, and so on), and CSV files. You supply a supported file type, and the Quick Look controller displays it for the user. An integrated system-supplied activity view controller helps share the previewed document, as you can see in Figure 11-6.

Image

Figure 11-6 This Quick Look controller is presented modally and shows the screen after the user has tapped the Action button. Quick Look handles a wide range of document types, enabling users to see the file contents before deciding on an action to apply to them.

Either push or present your preview controllers. The controller adapts to both situations, working with navigation stacks and with modal presentation. Recipe 11-5 demonstrates both approaches.

Implementing Quick Look

Implementing Quick Look requires just a few simple steps:

1. Declare the QLPreviewControllerDataSource protocol in your primary controller class.

2. Implement the numberOfPreviewItemsInPreviewController: and preview-Controller:previewItemAtIndex: data source methods. The first of these methods returns a count of items to preview. The second returns the preview item referred to by the index.

3. Ensure that preview items conform to the QLPreviewItem protocol. This protocol consists of two required properties: a preview title and an item URL. Recipe 11-5 creates a conforming QuickItem class. This class implements an absolutely minimal approach to support the data source.

Once these requirements are met, your code is ready to create a new preview controller, set its data source, and present or push it.

Recipe 11-5 Quick Look


@interface QuickItem : NSObject <QLPreviewItem>
@property (nonatomic, strong) NSString *path;
@property (readonly) NSString *previewItemTitle;
@property (readonly) NSURL *previewItemURL;
@end

@implementation QuickItem

// Title for preview item
- (NSString *)previewItemTitle
{
return [_path lastPathComponent];
}

// URL for preview item
- (NSURL *)previewItemURL
{
return [NSURL fileURLWithPath:_path];
}
@end

#define FILE_PATH [NSHomeDirectory() \
stringByAppendingPathComponent:@"Documents/PDFSample.pdf"]

@interface TestBedViewController : UIViewController
<QLPreviewControllerDataSource>
@end

@implementation TestBedViewController
- (NSInteger)numberOfPreviewItemsInPreviewController:
(QLPreviewController *)controller
{
return 1;
}

- (id <QLPreviewItem>)previewController:
(QLPreviewController *)controller
previewItemAtIndex:(NSInteger)index;
{
QuickItem *item = [[QuickItem alloc] init];
item.path = FILE_PATH;
return item;
}

// Push onto navigation stack
- (void)push
{
QLPreviewController *controller =
[[QLPreviewController alloc] init];
controller.dataSource = self;
[self.navigationController
pushViewController:controller animated:YES];
}

// Use modal presentation
- (void)present
{
QLPreviewController *controller =
[[QLPreviewController alloc] init];
controller.dataSource = self;
[self presentViewController:controller
animated:YES completion:nil];
}

- (void)loadView
{
self.view = [[UIView alloc] init];
self.view.backgroundColor = [UIColor whiteColor];

self.navigationItem.rightBarButtonItem =
BARBUTTON(@"Push", @selector(push));
self.navigationItem.leftBarButtonItem =
BARBUTTON(@"Present", @selector(present));
}
@end



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 11.


Recipe: Using the Document Interaction Controller

The UIDocumentInteractionController class enables applications to present interaction options to users, enabling them to use document files in a variety of ways. With this class, users can take advantage of the following:

Image iOS application-to-application document sharing (that is, “Open this document in...some app”)

Image Document preview using Quick Look

Image Activity controller options such as printing, sharing, and social networking

You’ve already seen the latter two features in action in the activity view controller earlier in this chapter. In both presentation and behavior, UIDocumentInteractionController is actually very similar to UIActivityViewController. The document interaction class adds a powerful app-to-app sharing capability.

The controller offers two styles, as shown in Figure 11-7. The “Open in...” style offers only “Open in” choices. The “options” style provides a list of interaction options, including “Open in...,” Quick Look, and any supported actions. It’s essentially all the good stuff you get from a standard Actions menu, along with “Open in...” extras. You do have to explicitly add Quick Look callbacks, but doing so takes little work.

Image

Figure 11-7 The UIDocumentInteractionController shown in its “Open in...” style (left) and options style (right).

Creating Document Interaction Controller Instances

Each document interaction controller is specific to a single document file. This file is typically stored in the user’s Documents folder, represented by the fileURL in this snippet:

dic = [UIDocumentInteractionController
interactionControllerWithURL:fileURL];

You supply a local file URL and present the controller using either the “options” variation (basically the Action menu) or the “Open in...” style. Present the Options menu in one of these two styles from a bar button or an onscreen rectangle:

Image presentOptionsMenuFromRect:inView:animated:

Image presentOptionsMenuFromBarButtonItem:animated:

Image presentOpenInMenuFromRect:inView:animated:

Image presentOpenInMenuFromBarButtonItem:animated:

The iPad uses the bar button or rect you pass to present a popover. On the iPhone, the implementation presents a modal controller view. As you would expect, more bookkeeping takes place on the iPad, where users may tap on other bar buttons, may dismiss the popover, and so forth.

Disable each iPad bar button item after presenting its associated controller and re-enable it after dismissal. This is important because you don’t want your user to re-tap an in-use bar button and need to handle situations where a different popover needs to take over. Basically, there are a variety of unpleasant scenarios that can happen if you don’t carefully monitor which buttons are active and what popover is in play. Recipe 11-6 guards against these scenarios.

Document Interaction Controller Properties

Each document interaction controller offers a number of properties, which can be used in your controller delegate callbacks:

Image URL—This property enables you to query the controller for the file it is servicing. This is the same URL you pass when creating the controller.

Image UTI—This property is used to determine which apps can open the document. It uses the system-supplied functions discussed earlier in the chapter to find the most preferred UTI match, based on the filename and metadata. You can override this in code to set the property manually.

Image name—This property provides the last path component of the URL, offering a quick way to provide a user-interpretable name without having to manually strip the URL yourself.

Image icons—Use this property to retrieve an icon for the file type that’s in play. Applications that declare support for certain file types provide image links in their declaration (as you’ll see shortly, in the discussion about declaring file support). These images correspond to the values stored for the kUTTypeIconFileKey key, as mentioned earlier in this chapter.

Image annotation—This property provides a way to pass custom data along with a file to any application that will open the file. There are no standards for using this property; however, the item must be set to some top-level property list object—namely dictionaries, arrays, data, strings, numbers, and dates. Because there are no community standards, use of this property tends to be minimal except where developers share the information across their own suite of published apps.

Providing Document Quick Look Support

Add Quick Look support to the controller by implementing a trio of delegate callbacks:

#pragma mark QuickLook
- (UIViewController *)
documentInteractionControllerViewControllerForPreview:
(UIDocumentInteractionController *)controller
{
return self;
}

- (UIView *)documentInteractionControllerViewForPreview:
(UIDocumentInteractionController *)controller
{
return self.view;
}

- (CGRect)documentInteractionControllerRectForPreview:
(UIDocumentInteractionController *)controller
{
return self.view.frame;
}

These methods declare which view controller will be used to present the preview, which view will host it, and the frame for the preview size. You may have occasional compelling reasons to use a child view controller with limited screen presence on tablets (such as in a split view, with the preview in just one portion), but for the iPhone family, there’s almost never any reason not to allow the preview to take over the entire screen.

Checking for the Open Menu

When you use a document interaction controller, the Options menu almost always provides valid menu choices, especially if you implement the Quick Look callbacks. You may or may not, however, have any “Open in...” options to work with. Those options depend on the file data you provide to the controller and the applications users install on their devices.

A no-open-options scenario happens when there are no applications installed on a device that support the file type you are working with. This may be caused by an obscure file type, but more often it occurs because the user has not yet purchased and installed a relevant application. This is a common occurrence when using the iOS simulator.

Always check whether to offer an “Open in...” menu option. Recipe 11-6 performs a rather ugly test to see if external apps will offer themselves as presenters and editors for a given URL. This is what it does: It creates a new temporary controller and attempts to present it. If it succeeds, conforming file destinations exist and are installed on the device. If not, there are no such apps, and any “Open in...” buttons should be disabled.

On the iPad, you must run this check in viewDidAppear: or later—that is, after a window has been established. The method immediately dismisses the controller after presentation. Your end user should not notice it, and none of the calls use animation.

This is obviously a rather dreadful implementation, but it has the advantage of testing as you lay out your interface or when you start working with a new file. File an enhancement request at bugreporter.apple.com.

One further caution: Although this test works on primary views (as in Recipe 11-6), it can cause headaches in nonstandard presentations in popovers on the iPad.


Note

You rarely offer users both option and “Open in...” items in the same application. Recipe 11-6 uses the system-supplied Action item icon for the Options menu. You may want to use this in place of “Open in...” text for apps that exclusively use the open style.


Recipe 11-6 Document Interaction Controllers


@implementation TestBedViewController
{
NSURL *fileURL;
UIDocumentInteractionController *dic;
BOOL canOpen;
}

#pragma mark QuickLook
- (UIViewController *)
documentInteractionControllerViewControllerForPreview:
(UIDocumentInteractionController *)controller
{
return self;
}

- (UIView *)documentInteractionControllerViewForPreview:
(UIDocumentInteractionController *)controller
{
return self.view;
}

- (CGRect)documentInteractionControllerRectForPreview:
(UIDocumentInteractionController *)controller
{
return self.view.frame;
}

#pragma mark Options / Open in Menu

// Clean up after dismissing options menu
- (void)documentInteractionControllerDidDismissOptionsMenu:
(UIDocumentInteractionController *)controller
{
self.navigationItem.leftBarButtonItem.enabled = YES;
dic = nil;
}

// Clean up after dismissing open menu
- (void)documentInteractionControllerDidDismissOpenInMenu:
(UIDocumentInteractionController *)controller
{
self.navigationItem.rightBarButtonItem.enabled = canOpen;
dic = nil;
}

// Before presenting a controller, check to see if there's an
// existing one that needs dismissing
- (void)dismissIfNeeded
{
if (dic)
{
[dic dismissMenuAnimated:YES];
self.navigationItem.rightBarButtonItem.enabled = canOpen;
self.navigationItem.leftBarButtonItem.enabled = YES;
}
}

// Present the options menu
- (void)action:(UIBarButtonItem *)bbi
{
[self dismissIfNeeded];
dic = [UIDocumentInteractionController
interactionControllerWithURL:fileURL];
dic.delegate = self;
self.navigationItem.leftBarButtonItem.enabled = NO;
[dic presentOptionsMenuFromBarButtonItem:bbi animated:YES];
}

// Present the open-in menu
- (void)open:(UIBarButtonItem *)bbi
{
[self dismissIfNeeded];
dic = [UIDocumentInteractionController
interactionControllerWithURL:fileURL];
dic.delegate = self;
self.navigationItem.rightBarButtonItem.enabled = NO;
[dic presentOpenInMenuFromBarButtonItem:bbi animated:YES];
}

#pragma mark Test for Open-ability
- (BOOL)canOpen:(NSURL *)aFileURL
{
UIDocumentInteractionController *tmp =
[UIDocumentInteractionController
interactionControllerWithURL:aFileURL];
BOOL success =
[tmp presentOpenInMenuFromRect:CGRectMake(0,0,1,1)
inView:self.view animated:NO];
[tmp dismissMenuAnimated:NO];
return success;
}

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Only enable right button if the file can be opened
canOpen = [self canOpen:fileURL];
self.navigationItem.rightBarButtonItem.enabled = canOpen;
}

#pragma mark View management
- (void)loadView
{
self.view = [[UIView alloc] init];
self.view.backgroundColor = [UIColor whiteColor];
self.navigationItem.rightBarButtonItem =
BARBUTTON(@"Open in...", @selector(open:));
self.navigationItem.leftBarButtonItem =
SYSBARBUTTON(UIBarButtonSystemItemAction,
@selector(action:));

NSString *filePath = [NSHomeDirectory()
stringByAppendingPathComponent:@"Documents/DICImage.jpg"];
fileURL = [NSURL fileURLWithPath:filePath];
}
@end



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 11.


Recipe: Declaring Document Support

Application documents are not limited to files an application creates or downloads from the Internet. As you discovered in Recipe 11-6, applications may handle certain file types. They may open items passed from other apps. You’ve already seen document sharing from the sending point of view, using the “open in” controller to export files to other applications. Now it’s time to look at it from the receiver’s end.

Applications declare their support for certain file types in their Info.plist property list. The Launch Services system reads this data and creates the file-to-app associations that the document interaction controller uses.

Although you can edit the property list directly, Xcode offers a simple form as part of the Project > Target > Info screen. Open the Document Types section, which is below the Custom iOS Target Properties. Click + to add a new supported document type. Figure 11-8 shows what this looks like for an app that accepts JPEG image documents.

Image

Figure 11-8 Declare supported document types in Xcode’s Target > Info screen.

This declaration contains three minimal details:

Image Name—The name is both required and arbitrary. It should be descriptive of the kind of document in play, but it’s also somewhat of an afterthought on iOS. This field makes more sense when used on a Macintosh (it’s the “kind” string used by Finder), but it is not optional.

Image One or more UTIs—Specify one or more UTIs as your types. This example specifies only public.jpeg. Add commas between items when listing several items. For example, you might have an image document type that opens public.jpeg, public.tiff, andpublic.png. Enumerate specific types when you need to limit file support. Although declaring public.image would cover all three types, it might allow unsupported image styles to be opened as well.

Image Handler rank—The launch services handler rank describes how the app views itself alongside the competition for handling this file type. Owner says that this is a native app that creates files of this type. Alternate, as in Figure 11-8, offers a secondary viewer. You add theLSHandlerRank key manually in the additional document type properties.

You may optionally specify icon files. These are used in OS X as document icons and have minimal overlap with the iOS world. The only case where you might see these icons is in the iTunes Apps tab when you’re using the File Sharing section to add and remove items. Icons are typically 320×320 (UTTypeSize320IconFile) and 64×64 (UTTypeSize64IconFile) and are normally limited to files that your app creates and for which it defines a custom type.

Under the hood, Xcode uses this interactive form to build a CFBundleDocumentTypes array in your application’s Info.plist. The following snippet shows the information from Figure 11-8 in its Info.plist form:

<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>jpg</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.jpeg</string>
</array>
</dict>
</array>

Creating Custom Document Types

When your application builds new kinds of documents, you should declare them in the Exported UTIs section of the Target > Info editor, which you see in Figure 11-9. This registers support for this file type with the system and identifies you as the owner of that type.

Image

Figure 11-9 Declare custom file types in the Exported UTIs section of the Target > Info editor.

To define the new type, supply a custom UTI (here, com.sadun.cookbookfile), document art (at 64 and 320 sizes), and specify a filename extension that identifies your file type. As with declaring document support, Xcode builds an exported declaration array into your project’sInfo.plist file. Here is what that material might look like for the declaration shown in Figure 11-9:

<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.text</string>
</array>
<key>UTTypeDescription</key>
<string>Cookbook</string>
<key>UTTypeIdentifier</key>
<string>com.sadun.cookbookfile</string>
<key>UTTypeSize320IconFile</key>
<string>Cover-320</string>
<key>UTTypeSize64IconFile</key>
<string>Cover-64</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<string>cookbook</string>
</dict>
</dict>
</array>

If you add this to your project, your app should open any files with the cookbook extension, using the com.sadun.cookbookfile UTI.

Implementing Document Support

When your application provides document support, you should check for an Inbox folder each time it becomes active:

- (void)applicationDidBecomeActive:(UIApplication *)application
{
// perform inbox test here
}

Specifically, see if an Inbox folder has appeared in the Documents folder. If it has, you should move elements out of that Inbox to where they belong, typically in the main Documents directory. After the Inbox has been cleared, delete it. This provides the best user experience, especially in terms of any file sharing through iTunes, where the Inbox and its role may confuse users.

When moving items to Documents, check for name conflicts and use an alternative path name (typically by appending a hyphen followed by a number) to avoid overwriting any existing file. Recipe 11-7 helps find an alternative name for a destination path. It gives up after a thousand attempts. Seriously, none of your users should be hosting that many duplicate document names. If they do, there’s something deeply wrong with your overall application design.

Recipe 11-7 walks through the ugly details of scanning for the Inbox and moving files into place. It removes the Inbox after it is emptied. As you can see, any method like this is File Manager–intensive. It primarily involves handling all the error combination possibilities that might pop up throughout the task. Processing the Inbox should run quickly for small file support. If you must handle large files, such as video or audio, make sure to perform this processing on its own operation queue.

If you plan to support public.data files (which will open anything), you might want to display those files by using UIWebView instances. Refer to Technical Q&A QA1630 (http://developer.apple.com/library/ios/#qa/qa1630) for details about which document types iOS can and cannot display in those views. Web views can present most audio and video assets, as well as Excel, Keynote, Numbers, Pages, PDF, PowerPoint, and Word resources, in addition to simple HTML.

Recipe 11-7 Handling Incoming Documents


#define DOCUMENTS_PATH [NSHomeDirectory() \
stringByAppendingPathComponent:@"Documents"]
#define INBOX_PATH [DOCUMENTS_PATH \
stringByAppendingPathComponent:@"Inbox"]

@implementation InboxHelper
+ (NSString *)findAlternativeNameForPath:(NSString *)path
{
NSString *ext = path.pathExtension;
NSString *base = [path stringByDeletingPathExtension];

for (int i = 1; i < 999; i++)
{
NSString *dest =
[NSString stringWithFormat:@"%@-%d.%@", base, i, ext];

// if the file does not yet exist, use this destination path
if (![[NSFileManager defaultManager]
fileExistsAtPath:dest])
return dest;
}

NSLog(@"Exhausted possible names for file %@. Bailing.",
path.lastPathComponent);
return nil;
}

- (void)checkAndProcessInbox
{
// Does the Inbox exist? If not, we're done
BOOL isDir;
if (![[NSFileManager defaultManager]
fileExistsAtPath:INBOX_PATH isDirectory:&isDir])
return;

NSError *error;
BOOL success;

// If the Inbox is not a folder, remove it
if (!isDir)
{
success = [[NSFileManager defaultManager]
removeItemAtPath:INBOX_PATH error:&error];
if (!success)
{
NSLog(@"Error deleting Inbox file (not directory): %@",
error.localizedFailureReason);
return;
}
}

// Retrieve a list of files in the Inbox
NSArray *fileArray = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:INBOX_PATH error:&error];
if (!fileArray)
{
NSLog(@"Error reading contents of Inbox: %@",
error.localizedFailureReason);
return;
}

// Remember the number of items
NSUInteger initialCount = fileArray.count;

// Iterate through each file, moving it to Documents
for (NSString *filename in fileArray)
{
NSString *source = [INBOX_PATH
stringByAppendingPathComponent:filename];
NSString *dest = [DOCUMENTS_PATH
stringByAppendingPathComponent:filename];

// Is the file already there?
BOOL exists =
[[NSFileManager defaultManager] fileExistsAtPath:dest];
if (exists) dest = [self findAlternativeNameForPath:dest];
if (!dest)
{
NSLog(@"Error. File name conflict not resolved");
continue;
}

// Move file into place
success = [[NSFileManager defaultManager]
moveItemAtPath:source toPath:dest error:&error];
if (!success)
{
NSLog(@"Error moving file from Inbox: %@",
error.localizedFailureReason);
continue;
}
}

// Inbox should now be empty
fileArray = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:INBOX_PATH error:&error];
if (!fileArray)
{
NSLog(@"Error reading contents of Inbox: %@",
error.localizedFailureReason);
return;
}

if (fileArray.count)
{
NSLog(@"Error clearing Inbox. %d items remain",
fileArray.count);
return;
}

// Remove the inbox
success = [[NSFileManager defaultManager]
removeItemAtPath:INBOX_PATH error:&error];
if (!success)
{
NSLog(@"Error removing inbox: %@",
error.localizedFailureReason);
return;
}

NSLog(@"Moved %d items from the Inbox", initialCount);
}
@end



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 11.


Recipe: Creating URL-Based Services

Apple’s built-in applications offer a variety of services that can be accessed via URL calls. You can ask Safari to open web pages, open Maps to show a map, or use the mailto:-style URL to start composing a letter in Mail. A URL scheme refers to the first part of the URL that appears before the colon, such as http or ftp.

These services work because iOS knows how to match URL schemes to applications. A URL that starts with http: opens in Mobile Safari. The mailto: URL always links to Mail. What you may not know is that you can define your own URL schemes and implement them in your applications. Not all standard schemes are supported on iOS. For example, the FTP scheme is not available for use.

Custom schemes enable applications to launch whenever Mobile Safari or another application opens a URL of that type. For example, if your application registers xyz, xyz: links go directly to your application for handling, where they’re passed to the application delegate’s URL opening method. You do not have to add any special coding there. If all you want to do is run an application, adding the scheme and opening the URL enables cross-application launching.

Handlers extend launching to allow applications to do something with the URL that’s been passed to it. They might open a specific data file, retrieve a particular name, display a certain image, or otherwise process information included in the call.

Declaring the Scheme

To declare your URL scheme, edit the URL Types section of the Target > Info editor (see Figure 11-10) and list the URL schemes you will use. The Info.plist section created by this declaration looks like this:

<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.sadun.urlSchemeDemonstration</string>
<key>CFBundleURLSchemes</key>
<array>
<string>xyz</string>
</array>
</dict>
</array>

Image

Figure 11-10 Add custom URL schemes in the URL Types section of the Target > Info editor.

The CFBundleURLTypes entry consists of an array of dictionaries that describe the URL types the application can open and handle. Each dictionary is quite simple. Each one contains two keys: a CFBundleURLName (which defines an arbitrary identifier) and an array ofCFBundleURLSchemes.

The schemes array provides a list of prefixes that belong to the abstract name. You can add one scheme or many. This following example declares just one. You might want to prefix your name with an x (for example, x-sadun-services). Although the iOS family is not part of any standards organization, an x prefix indicates that this is an unregistered name. A draft specification for x-callback-url is under development at http://x-callback-url.com.

A number of informal registries have popped up so iOS developers can share their schemes in central listings. You can discover services you want to use and promote services you offer. Each registry lists services and their URL schemes and describes how other developers can use these services. Some of these registries include http://handleopenurl.com, http://wiki.akosma.com/IPhone_URL_Schemes, and http://applookup.com/Home.

Testing URLs

You can test whether a URL service is available. If the UIApplication’s canOpenURL: method returns YES, you are guaranteed that openURL: can launch another application to open that URL:

if ([[UIApplication sharedApplication] canOpenURL:aURL])
[[UIApplication sharedApplication] openURL:aURL];

You are not guaranteed that the URL is valid—only that its scheme is registered properly to an existing application.

Adding the Handler Method

To handle URL requests, you implement the URL-specific application delegate method shown in Recipe 11-8. Unfortunately, this method is guaranteed to trigger only when the application is already running. If the app is not running and the app is launched by the URL request, control first goes to the launching methods (will- and did-finish).

You want to ensure that your normal application:didFinishLaunchingWithOptions: method returns YES. This allows control to pass to application:openURL:sourceApplication:annotation:, so the incoming URL can be processed and handled.

Recipe 11-8 Providing URL Scheme Support


// Called if the app is open or if didFinishLaunchingWithOptions returns YES
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
NSString *logString = [NSString stringWithFormat:
@"DID OPEN: URL[%@] App[%@] Annotation[%@]\n",
url, sourceApplication, annotation];
tbvc.textView.text =
[logString stringByAppendingString:tbvc.textView.text];
return YES;
}

// Make sure to return YES
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
_window = [[UIWindow alloc]
initWithFrame:[[UIScreen mainScreen] bounds]];
tbvc = [[TestBedViewController alloc] init];

UINavigationController *nav = [[UINavigationController alloc]
initWithRootViewController:tbvc];
window.rootViewController = nav;
[window makeKeyAndVisible];
return YES;
}



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 11.


Summary

Want to share data across applications and leverage system-supplied actions? This chapter shows you how. You’ve read about UTIs and how they are used to specify data roles across applications. You’ve seen how the pasteboard worked and how to share files with iTunes. You’ve read about monitoring folders and discovered how to implement custom URLs. You’ve dived deep into the activity view controller and document interaction controller, and you’ve seen how to add support for everything from printing to copying to previews. Here are a few thoughts to take with you before leaving this chapter:

Image You are never limited to the built-in UTIs that Apple provides, but you should follow Apple’s lead when you decide to add your own. Be sure to use custom reverse domain naming and add as many details as possible (public URL definition pages, typical icons, and file extensions) in your exported definitions. Precision matters.

Image Conformance arrays help you determine what kind of thing you’re working with. Knowing that you’re working with an image and not, say, a text file or movie, can help you better process the data associated with any file.

Image The Documents folder belongs to the user and not to you. Remember that and provide respectful management of that directory.

Image When you’re looking for one-stop shopping for data sharing, you’ll be hard-pressed to find a better solution than an activity view controller. Easy to use, and simple to present, this single controller does the work of an army, integrating your app with iOS’s system-supplied services.

Image For a lot of reasons, many developers used custom URL schemes in the past, but the document interaction controller often provides a better alternative. Use this controller to provide the app-to-app interaction your users demand and don’t be afraid of introducing annotation support to help ease the transition between apps.

Image Don’t offer an “Open in...” menu option unless there are onboard apps ready to back up that button. The solution you read about in this chapter is crude, but using it is better than dealing with angry, frustrated, or confused users through customer support. Consider providing an alert, backed by this method, that explains when there are no other apps available.