Detecting MIME type and file extension in Swift (using Uniform Type Identifiers)

swift ios macos matrix entropy

Why do we want to detect the MIME type or file extension?

The Matrix spec defines several messaging types for attachments (essentially binary blobs). The generic m.file can be used to send any type of file as an attachment. Common file types like audio, video, and especially images however have their own message type (m.audio, m.video and m.image respectively). So when sending images (or video, or audio) we want to send it using the appropriate message type.

One way of trying to determine, what kind of attachment we are sending is the extension. While this is not 100% accurate, it’s a good first guess. However, we will need to maintain some kind of list, mapping extension (or MIME type) to message type. Nobody likes maintaining lists like that as it’s easy to forget a type or make mistakes. So I thought there must already be a predefined list somewhere. Turns out, Apple’s OSs do.

System-Declared Uniform Type Identifiers

Apple has this fund little list of what they call “System-Declared Uniform Type Identifiers” where all the known file types are listed (and their respective constants like kUTTypePNG). You can also add your own using the provided APIs (and benefit from any other application adding their definition, I believe).

When trying to look into how to actually use them however, things get a bit iffy. So let’s have a look at the API.

Let’s try to create a UTI from a file extension in Swift:

NOTE: In order to use these in iOS you’ll need to import MobileCoreServices

1let fileExtension: CFString = "png"
2let extUTI = UTTypeCreatePreferredIdentifierForTag(
3    kUTTagClassFilenameExtension,
4    fileExtension,
5    nil
6)?.takeUnretainedValue()

UTTypeCreatePreferredIdentifierForTag returns an Unmanaged<CFString>?. If you don’t know what Unmanaged is, I recommend reading the NSHipster Guide on Unmanaged.

This will essentially return nil, if the extensions was not recognized, or a new CFString (of which we’ll want to take an unretained value in most cases).

Now that we have a UTI for this file extension we can use it to check conformity to a type class or get its associated MIME type.

 1let fileExtension: CFString = "png"
 2guard
 3    let extUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, nil)?.takeUnretainedValue()
 4else { return }
 5
 6guard
 7    let mimeUTI = UTTypeCopyPreferredTagWithClass(extUTI, kUTTagClassMIMEType)
 8else { return }
 9
10print(mimeUTI) // will print 'image/png'

UTTypeCopyPreferredTagWithClass can be used to transfrom one UTI to another tag class, which we are doing here.

We can try to get an appropriate file extension for a given mime type too:

 1let mimeType: CFString = "image/png"
 2guard
 3    let mimeUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, nil)?.takeUnretainedValue()
 4else { return }
 5
 6guard
 7    let extUTI = UTTypeCopyPreferredTagWithClass(mimeUTI, kUTTagClassFilenameExtension)
 8else { return }
 9
10print(extUTI) // will print 'png'

Lastly let’s use UTTypeConformsTo to check if our extension is an image:

1let mimeType: CFString = "image/png"
2guard
3    let mimeUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, nil)?.takeUnretainedValue()
4else { return }
5
6print(UTTypeConformsTo(mimeUTI, kUTTypeImage)) // will print `true`

So this is how you can use Apple’s “System-Declared Uniform Type Identifiers” to detect the extension, MIME type and conformity of files in Swift. If you’re interested in the full code and tests, check out the repo

Direct link to the file at the time of writing: here And the tests: here