Ever wonder how the system decides what app should open a given file? Explore the Uniform Type Identifiers framework, which helps you simplify the process for supporting standard or proprietary file formats in your app. You'll learn how to use the new framework and Xcode to declare the types your app supports, discover how to boost performance when you adopt UTTypes, and review the latest platform APIs that support UTTypes.
Hello and welcome to "A Reintroduction to Uniform Type Identifiers." My name is Jonathan Grynspan, and I work on the App Discovery and Engagement Team at Apple.
The last time we talked about uniform type identifiers was at WWDC 2004. That was a long time ago, but I remember it like it was yesterday.
Apple's 2004 product lineup was classic cool, with products like the all-aluminum design of the PowerBook G4, the eye-popping colors of the iPod Mini and the magical home theater experience of the iMac G5. Apple has stayed awesome, but you've got to admit 2004 really was a long time ago.
So we're going to cover the basics today, starting with how Apple's operating systems determine the type of a file.
Then we'll explore how to tell the system about new file types it doesn't know about and how to handle them.
Finally, we'll explore some new API in iOS 14 and macOS Big Sur that makes working with file types a breeze.
Whether you're new to the subject or not, we have something for you, so stick around.
Let's take a look at a file that could be on anybody's computer: a photo of a cat using an iPad Mini.
When you save that picture to disk, it produces what's called a regular file. That's POSIX terminology for a sequence of bytes stored on disk.
Later on, when you want to open this photo again, you can double-click it and it will just work, opening in an image viewer like Preview. But how does the system know that this is an image, in this case saved as JPEG, rather than a text file or an MP3 track or a Pages document? Files are sequences of bytes, so you might expect that when the system needs to know the type of a file, it cracks it open and reads the bits inside to figure it out.
Actually, the system almost never does that because it's extremely expensive and requires read permissions most processes don't have.
In fact, the operating system bases most of its decisions on the file's path extension. That is the case-normalized substring following the last period character, so long as it does not contain any white space or control characters. On Apple's platforms, we don't typically show recognized path extensions to the user, but they're still there. If we reveal this file's path extension, we can see that it is J-P-E-G, so this is a JPEG image.
So .jpeg means a JPEG image, right? Yes, but there are other path extensions for JPEG images. .jpg and .jpe are the most common.
On the web, however, it's a different story. Web servers don't generally identify files by their path extension. Instead, they use something called a MIME type or a media type.
For JPEG, that's image/jpeg. Of course, nothing in life is ever that simple, and some servers instead use the common but nonstandard image/jpg MIME type. That's five different pieces of metadata, all representing exactly the same thing. Well, on Apple's platforms, that's okay, because we use a single string called a uniform type identifier to canonically identify this file format.
For JPEG images, the uniform type identifier is public.jpeg. This one string refers to all JPEG images, whether they're local or on the web.
Now, an interesting property of file types is that they exist in a hierarchy. Every JPEG image is also more abstractly an image, as are PNG images, TIFF images, et cetera. When talking about uniform type identifiers, we say that the JPEG type conforms to the more abstract image type.
This conformance tree implicitly allows for multiple inheritance. If you think about it, this is sort of like how protocols work in Swift and Objective-C.
A concrete class or struct can inherit from any number of protocols. Let me show you what I mean.
Here's public.jpeg. And here's public.image, which is an abstract type that describes all image file formats. But all image file formats can be represented as sequences of bytes.
So public.image conforms to public.data, which is a very abstract type representing any regular file on disk, or really any sequence of bytes, regardless of where they're stored.
In turn, public.data conforms to one final type, public.item.
All file system objects, be they files, folders, symlinks or even something more esoteric such as POSIX pipes, are represented by public.item.
Just as we can go up the hierarchy, we can travel down it. There are image formats other than JPEG, such as public.png and public.tiff, and they are also subtypes of public.image. However, they are not part of the conformance hierarchy of public.jpeg. They're just its siblings. If we move up a level and focus on public.data, we can see that there are many types that conform to it, because nearly everything on your computer can be represented by a sequence of bytes. Here, we see public.audio, which is an abstract type describing audible sounds, and public.text, which is an abstract type describing legible text. They are subtypes of public.data and siblings of public.image.
I mentioned a while back that multiple inheritance is possible with uniform type identifiers. And that's true of images because images are data but they're also content that the user cares about, something the user might want to upload to iCloud Drive or share over AirDrop.
So public.image also conforms to public.content, which doesn't represent anything concrete in the file system, but does tell us that we should treat its subtypes as things that are important to the user.
I focused pretty heavily on how uniform type identifiers represent files. That's probably the most common use for them, as far as users are concerned. But we use them throughout our platforms for other purposes too. For instance, we also use them as the canonical type of pasteboard content. After all, if you can save it to disk, you can copy and paste it too.
We also use them for hierarchies that have nothing to do with files. This is a bit of an Apple secret, but we also use uniform type identifiers to identify different models of hardware we ship. All Mac models, for instance, conform to com.apple.mac. And if we want to get hypothetical, you could even use uniform type identifiers to refer to hierarchies unrelated to computers, such as the tree of life.
That's what uniform type identifiers are and how we use them on our platforms. Many apps create and maintain their own data formats, and these formats deserve their own unique types. When you create a new type, how do you add it to the type hierarchy? If you're using types declared by the system, you don't need to do much. We include a large number of types in a bundle called CoreTypes in /System/Library/CoreServices.
Any uniform type identifier in that bundle's Info.plist can be referenced by your app without additional changes.
But for types you invented or which you're borrowing from another app, you may need to tell the system about them. To do so, you need to create your own uniform type identifier.
When creating your own uniform type identifier, there are a few naming rules to follow. First, uniform type identifiers are always case-insensitive ASCII and always reverse-DNS, such as com.example.file.
Ideally, you'll use some more descriptive identifiers. com.example.file doesn't really make it easy to debug. com.example.imagetemplate or com.example.encrypteddatabase or something like that would be better.
Apple reserves some prefixes or namespaces in identifiers, and you should not create your own identifiers that use these namespaces. The system may ignore them if you do.
First is "public," which is reserved for use by Apple to declare standardized types. If we're missing a standardized type, please let us know via the Feedback Assistant.
dyn, short for dynamic, is reserved for use by the operating system when generating compatibility shims. They are typically opaque strings, and they can change between OS releases, so don't hard-code their values.
In practice, they're fairly rare these days. You typically only see them when encountering a file with an unrecognized path extension.
com.example is reserved for templates, examples, sample code and the like.
Finally, com.apple is reserved for use by Apple.
There are two broad steps you need to take when adding a type to your application that the system will recognize. The first step is to declare the type. Declaring a type basically means your app is saying, "This type exists," but doesn't mean you can open the type.
If you actually want to open a type, you need to support the type, which is your app's way of saying, "No matter who declared the type, I can open it." These two steps are distinct.
When you declare your type, you need to decide whether you will import or export it. If you are using a type that was invented or designed by somebody else or for use primarily with another application, you should typically import the type. That tells the system, "This type exists. Here's some information about it." But if the app that invented it is installed, that app can provide more authoritative information.
On the other hand, if you invented the type or it's meant for use specifically with your app, it is appropriate to export it. This tells the system, "I am authoritative for this type." Finally, if you are using a type that ships with the system as part of Core Types, you don't need to import or export it. The system has already provided a declaration, and you can just use the type's uniform type identifier immediately.
I'd like to show you how to declare a type in your app and how to show the system you support opening it. So let's take a look at Xcode, where I'm hard at work on a brand-new restauranting-over-the-web experience.
This app has the ability to read restaurant menus that have been saved in a JSON-based format we've designed. We want to be able to open those menu files in our app, so we'll need to first declare a type for them. Because we created this type ourselves and we own the format, it makes sense for us to export the type. The first thing we'll do in Xcode is select the project itself to reveal its settings.
We want to select the target corresponding to our app. Right now, I'm looking at our iOS app, but we'd do the same thing for a macOS app. Then we'll switch over to the Info tab, which represents the contents of an app's Info.plist file. Expand the Exported Type Identifiers section. Click the Add button at the bottom of the section to add a new type. The first thing we want to add is the uniform type identifier we came up with.
For this demo, we'll use com.example.restaurantmenu.
Remember, com.example is reserved by Apple for use in examples and demos. You'll usually want to use a reverse DNS name that you own. For instance, the company Claris would use com.claris, while Beats would use com.beatsbydre.
Now we have to think about conformance.
Types that represent files in the file system need to conform to public.data if they're regular files-- that is, sequences of bytes-- or to com.apple.package if they're directories that the operating system should treat as files. This type is going to be saved to disk using Swift's JSON encoder, which means it's a sequence of bytes. That means regular file, and that means public.data.
Looks like we have a decision to make. Because these files will contain JSON data, they will be readable by anything that can read a JSON file. That means the type can also conform to public.json.
That's a really useful property for some file types where the ability to edit them manually is important. But for other types, it's an implementation detail only.
We'll add conformance to public.json for this type. You can skip these sorts of implementation details when creating your types if you don't want other developers to rely on them or if you expect your type's format may change in the future. This file type is something the user would consider a document or content worth interacting with directly.
So we'll also make sure it conforms to public.content.
Next, we'll need to decide on a file name extension to associate with our type.
This extension will tie the type back into the file system and allow the system to recognize that appropriately named files have this type.
We may be tempted to use a three-character extension, but no major platform vendor has required three-character extensions since 1995. We have lots of room to breathe here, and avoiding conflicts with other developers' type declarations is important. So we'll use a longer extension, restaurantmenu. Note that we didn't put a dot there. The operating system knows to add it for us when creating file names. Lastly, this is also a good opportunity to come up with a human-readable name for this type. Since our app is simply called Restaurant, and since we picked the extension restaurantmenu, we'll keep this simple and name it Restaurant Menu.
This string is localizable in our InfoPlist.strings file.
Use the string we just typed as your key when localizing. Now that we've declared the type, we need to tell the system that our app is able to open it. Expand the Document Type section.
Click the Add button at the bottom of the section to add a new supported type.
This section is simpler than the Exported Type Identifier section. The only thing we're required to specify is the list of uniform type identifiers the app supports. To allow for flexibility, we can specify multiple types for a single entry here or one per entry.
I personally prefer having one type per entry, but it's mostly a question of personal preference, like tabs versus spaces.
Anyway, we'll add our uniform type identifier to this field.
We should also set this entry's handler rank to Owner. This step isn't required but it's strongly recommended because it helps the system intelligently pick the right app for a given job. Since we invented this type, we own it.
On macOS, there is an additional Rule field that lets us specify whether we can open files as an editor or a viewer. Editors can both open and save files of a given type, while viewers can only open them. Because our app can both read and write these files, we'd specify editor. We're done editing the Info section. Our app can now open our Restaurant Menu files. We'll need to write some SwiftUI, UIkit or AppKit code to handle the document once the system passes it to us.
For more information on building a document-based application in Swift UI, check out "Build Document-Based Apps in SwiftUI" from WWDC20. Documentation for building document-based applications using UIkit and AppKit is available online at developer.apple.com.
Our app has a competitor on the App Store, Compy's Food. Lots of restaurant owners use that app and have saved their menus in the file format owned by that app. We want to also support reading those files. This type was invented by somebody else, and we're only borrowing it, so we need to import it instead of export it. Let's head back to the Info tab for our app target and select Imported Type Identifiers. Note that because we created a new SwiftUI app, we have an existing entry here for an example type. We can replace the contents of the form with our own data, which is what I'll do. If we didn't have an existing example type in our target, we could of course add a new type with the Add button below.
I'll fill in the Info section specified by Compy's Food.
Of course, the details here are examples only. If you're adding support for a real competitor's type, you should strive to match what they've included in their Info.plist file.
Because this type is imported, it tells the system about the sort of files produced by the Compy's Food app. But if the user has that app installed, the system can prefer its declaration, which should be exported as more authoritative.
We'll now add support for opening these files so that the system can pick our app when the user wants to open one of them. We'll go back to the Document Type section.
We'll create a separate entry for Compy's Food List and add the new type identifier.
This entry is a little different from the other one because we're not the owner of the type. Instead, we want our handler rank to be Alternate.
Because our app can read these files but not write them, we'd specify a role of Viewer on macOS.
The system now knows we can open this file but that we may not be the best choice if the user has Compy's Food installed, since that app is the owner of the file type. We want to be good citizens on the platform, so it's important to respect type ownership this way even though we both know our code is way better than our competitor's code. Just as we did with our own restaurant menu type, we'll need to write some Swift or Objective-C to actually read these files in our app. Check out developer.apple.com for more info.
So now that we've declared a type and told the system we support it, it's time to work with that type in our code. This section assumes you're at least broadly familiar with the uniform type identifier's API in the Core Services framework. Here we have a small program that walks a directory or a set of file URLs. It gets the uniform type identifier of each represented file and then prints a localized description of that type identifier. If the file is an image, it draws it somewhere, and if it's an audio clip, it plays it over the user's speakers or AirPods. Right away, there are some problems visible here. We have to explicitly cast our strings to CFStrings to use them with the global UTType functions. And we have to manually manage the lifetime of the CFString we get from UTTypeCopyDescription.
But, and I'm very happy to talk to you about this, in iOS 14 and macOS Big Sur, we have introduced a brand-new uniform type identifiers framework that brings first-class Swiftiness to uniform type identifiers. Where we previously used CFStrings to represent type identifiers, we can now use UTType to represent types and their properties. Let's walk through this code and convert it to use the new framework.
I've imported the framework in order to get access to the API it contains, and now I'll change the type identifier URL property to contentType, whose value is an instance of a new struct, UTType.
This struct encapsulates a uniform type identifier alongside metadata about it. Next, we'll fix this unretained value.
UTType has a localized description property. Properties like this one are a much more natural way to work with the type's attributes and global functions. The value of this property corresponds to the description string you entered in Xcode, and is localized when you localize your app.
Next, let's turn to UTTypeConformsTo, another global function.
This function takes the CFString, which necessitates explicit casts.
Unsurprisingly, UTType has a conforms-to member method. But what are these: .image and .audio? Remember this slide? When we were designing this new framework, we took a look at the set of types declared by the system as well as the set of CFString constants exposed in the old API, and we brought them over to the new framework as named constants. There are over 120 such constants defined in the new framework, covering the most commonly used system-defined types. Of course, earlier in this session, we declared some types in our app. Wouldn't it be nice if we could expose constants for them too? I'm just full of good news today, because you can do exactly that. We have API for declaring named constants for types that you export and for types that you import. The semantics are a little different for each. When you export a type, you're saying, "I own this type. I created it. My app is the authority for this type." So we go ahead and create an instance of UTType for you, safe in the knowledge that you know everything about this type.
When you import a type, you're saying, "I know about this type, but somebody else might know more than me," and the owner of that type might declare the type differently than you did.
If that happens, we can substitute that type on the fly in the most common use cases. We need your cooperation though. Rather than declaring your imported types as constants, declare them as static computed variables, so that if the declaration changes due to an app installation, you'll pick up the updated type automatically.
We've built support into Xcode for detecting issues with types you declare. We can detect if an exported or imported type is missing from your Info.plist, or if you used the wrong constant initializer. We'll work to improve the support over time, and your input will be invaluable here, so please provide feedback. A number of frameworks support our new API in iOS 14 and macOS Big Sur. Foundation has the URL property I just showed you, and it also has utilities for generating recommended file names based on a string or URL and an instance of UTType.
SwiftUI has extensive support for UTType. Pasteboard and drag-and-drop APIs, such as the onDrop event handler, use them exclusively.
And SwiftUI's new support for document-based apps is designed to use UTType. I recommend you check out "Build Document-Based Apps in SwiftUI" from WWDC 20 to learn more about it.
When using AppKit, NSWorkspace will let you get an icon for any UTType. And NSOpenPanel and NSSavePanel support UTTypes. UIkit supports using UTTypes when presenting document pickers and document browsers. And Core Spotlight lets you create an attribute set from a UTType.
There are a number of APIs that do not support UTType in this release, and you may find yourself needing to interact with them. It's not difficult. If you need to pass a uniform type identifier to an API, get the UTType's identifier property and cast it to a CFString.
If you want to convert a uniform type identifier back to a UTType, cast it from CFString to String and initialize your UTType with that string. This is a failable initializer, so don't forget to check for nil.
In Objective-C, it's a little wordier, but the idea is the same. Cast the UTType's identifier to CFStringRef or cast a CFString to NSString and create a UTType from it.
We've seen today how the system derives a file's type from its metadata and how Apple encapsulates that information using uniform type identifiers.
We've learned how to declare and use our own types and how to interact with types that may be shared among multiple applications.
And we've introduced a new uniform type identifiers framework that provides a modern, object-oriented way to interact with types.
I look forward to seeing how you use this knowledge and the new framework to build your apps. Thank you.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.