camdez.com

Rule #1: There are no rules.

NSPasteboard and Dynamic UTIs

| Comments

When dealing with pasted or dragged data in Cocoa, we receive the passed data on an NSPasteboard. Prior to v10.6 (Snow Leopard), this pasteboard could only contain a single item, but now it can contain multiple items, which are returned as an NSArray via the -pasteboardItems method.

The data passed in via the pasteboard can be in a variety of types (for instance, a single image could be passed both in JPEG format and TIFF format), and for compatibility reasons these types can be expressed in various ways—they can be MIME types, UTIs, OSTypes, or PboardTypes.

If we want to handle pastes and drags in the proper, modern, multi-item fashion, you’d think we could just iterate over the -pasteboardItems (each is an NSPasteboardItem), ask each for its -types, and handle the data in our preferred format. Unfortunately things aren’t always that simple.

The Problem

Suppose we have an NSPasteboard named pboard, which has a single NSPasteboardItem (called item) on it. According to the NSPasteboard documentation, NSPasteboard’s -types method returns “an array of NSString objects containing the union of the types of data declared for all the pasteboard items on the receiver”.

Therefore, in this simple case, [pboard types] should return exactly the same types as [item types]. It turns out that it doesn’t.

Here’s a sample code snippet:

1
2
3
4
5
6
7
for (NSString* type in [pboard types])
  NSLog(@"Pasteboard type: %@", type);

NSPasteboardItem* item = [[pboard pasteboardItems] objectAtIndex:0];

for (NSString* type in [item types])
  NSLog(@"Item type: %@", type);

…which surprisingly prints the following when a URL is dropped onto it from Safari:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Pasteboard type: dyn.ah62d4rv4gu8zs3pcnzme2641rf4guzdmsv0gn64uqm10c6xenv61a3k
Pasteboard type: WebURLsWithTitlesPboardType
Pasteboard type: dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu
Pasteboard type: Apple URL pasteboard type
Pasteboard type: public.url
Pasteboard type: CorePasteboardFlavorType 0x75726C20
Pasteboard type: public.url-name
Pasteboard type: CorePasteboardFlavorType 0x75726C6E
Pasteboard type: public.utf8-plain-text
Pasteboard type: NSStringPboardType
Item type: dyn.ah62d4rv4gu8zs3pcnzme2641rf4guzdmsv0gn64uqm10c6xenv61a3k
Item type: dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu
Item type: public.url
Item type: public.url-name
Item type: public.utf8-plain-text

This might be OK if you’re looking for one of the types that is showing up as an “Item type”, but what if you are expecting one of those that isn’t?

An Explanation

Notice that there are exactly twice as many types in [pboard types] as there are in [item types], and that all of the item types share a common dot-separated format. This is our hint that there’s some correspondence here.

Apple’s intention for developers is that we move towards using only UTI-type identifiers in the future, and it seems that in the jump to the multi-item pasteboard they’ve silently dropped support for other types of identifiers. The exception to this rule is when using the old, single-item-style NSPasteboard methods (-types, -dataForType:, setData:forType:, etc.). These methods likely continue to exist purely for the sake of old code, but now implicitly operate on the first item on the pasteboard.

Since we only see UTIs when dealing with NSPasteboardItems directly, how are we to retrieve data an older application placed on the pasteboard if that data is not identified with a UTI? It turns out that’s what those dyn.(...) types are for—they’re dynamically-generated UTIs which correspond to type identifiers in different formats.

In the example above, dyn.ah6(...)a3k is the UTI way of identifying data in the WebURLsWithTitlesPboardType type, and dyn.ah6(...)bwu is the UTI for Apple URL pasteboard type.

(If you’re wondering, public.url, public.url-name, and public.utf8-plain-text are also UTIs, but they’re UTIs that are declared to correspond to the other types you see in the output, so they don’t need to be dynamically-generated. The basic UTI types are declared in the LaunchServices/UTCoreTypes.h header, and an application can declare its own types in its Info.plist file under the UTExportedTypeDeclarations key.)

The Solution

If you’re looking to cleanly handle data that an application placed on the pasteboard which is not identified by a UTI, you need to look up the dynamic UTI which corresponds to the identifier you have. In order to get the right UTI you’ll need to know not only the identifying string for the type, but also what kind of type it is (MIME, PboardType, etc.). These different kinds are represented by the kUTTagClassFilenameExtension, kUTTagClassMIMEType, kUTTagClassNSPboardType, and kUTTagClassOSType constants in LaunchServices/UTType.h. Once we have this info we simply need to pass it to UTTypeCreatePreferredIdentifierForTag() to get the UTI.

For instance, let’s say we take a look at the NSPasteboards -types, and we see that it’s passing in data in the WebURLsWithTitlesPboardType format. We want that data, but we’re iterating over the pasteboardItems, so we need the corresponding UTI. We can get it like so:

1
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, CFSTR("WebURLsWithTitlesPboardType"), kUTTypeData);

If we’d run this code in the process above, uti would now contain dyn.ah62d4rv4gu8zs3pcnzme2641rf4guzdmsv0gn64uqm10c6xenv61a3k.

That’s it! Now we have a UTI suitable for comparing against the NSPasteboardItem’s -types.

Note: Despite what the documentation suggests, passing NULL as the third argument to UTTypeCreatePreferredIdentifierForTag() does not return the correct dynamic UTI. You must be more specific and pass kUTTypeData.


I owe thanks to SIGPIPE 13’s UTI Problems post for the last bit about kUTTypeData (I’m not sure I’d ever have caught that one my own), and the fine folks at StackOverflow who gave me enough hints to puzzle the rest out.

Comments