OSX App Sandboxing & “Related Items” tutorial

Recently I’ve started developing an app for OS X to automatically download videos’ subtitles (mostly because that app I was using before stopped being free and started asking for 19,99$ to keep using it) and thus had to deal with all the app sandboxing stuff required to be able to publish on the Mac App store.

Therefore I went on the Apple’s developer web site and read all the documentation about app sandboxing… my first reaction has been “screw the Mac App Store, I will distribute my app the dear old way!” but then I realised that if I want to collect the money to get back to WWDC next year the Mac App Store might be of some help.

If you are not familiar with the concept of app sandboxing this is a quote from its dedicated page on the Apple’s dev web site:

Sandboxing your app is a great way to protect systems and users by limiting the privileges of an app to its intended functionality, increasing the difficulty for malicious software to compromise your users’ systems.

In practice you have to explicitly ask permission (or an “entitlement” to be more precise) for doing anything involving system resources (files and folders, network, bluetooth, audio, etc.). In the specific case of my subtitles app I needed:

This last point has been the most challenging one and is the reason I’m writing this tutorial (I want to avoid that other people have to endure what I had to).

Basically if an app is sandboxed and has asked for the entitlement that enables files reading & writing (there are actually 3 different entitlements related to user selected files: one for reading, one for reading/writing and one to execute them… this last one is not an option available through Xcode but It has to be manually added to the entitlements file) it can access only those files that have been opened using an NSOpenPanel or have been drag & dropped inside the app and can create new files only through a NSSavePanel.

Again that last point was clearly a usability issue for my app: if I have to open a NSSavePanel and manually save every subtitles file I’d rather go on open subtitles.org and manually download the subtitles from there (well… maybe not but still it would really piss me off)!

Lucky digging more in depth into the sandboxing documentation I found out a small section called “Related Items” which seems to describe exactly how to address my problem:

The related items feature of App Sandbox lets your app access files that have the same name as a user-chosen file, but a different extension.

Reading this really changed my day as my app wasn’t apparently destined anymore to a terrible user experience (at least for the app store version! Yes, before finding this I started considering a dual version strategy: one for download from a web site and one for the App Store)! In fact the subtitles files are file with the same name of the file video but with a different extension (.srt most of the times)

Albeit the section is not particularly clear in describing exactly what has to be done to write a related file it seems that there are two key points:

that the I have to add in my Info.plist, section CFBundleDocumentTypes, the document type of my “related item” (the subtitles) and add NSIsRelatedItemType = TRUE

I then read all the documentation about NSFileCoordinator/NSFilePresenter and how to add a document type to my info.plist… wrote the code… compiled… run… and… NOTHING! The app was not saving the subtitles file! So I Googled around… looked on stackoverflow… asked for help on the apple forums but nothing!

I was on the very verge of giving up on publishing the app on the Mac Apps Store! But then I told myself not to be lazy and seek more! And in the and, after digging in every documentation somehow related to my problem, I manage to let everything work!

To better explain how to deal with the “related items” I created a small demo project hosted on GitHub that you can look at to. This project build an app that allows a user to open a file using a NSOpenPanel and than write at the same path a related item file with an extension of choice.

Enough jiber jabbering for now and let’s begin with the tutorial!

First thing to do in your project is to activate the sandboxing: in Xcode go into the project preferences (blue project icon in the file navigator), select the “Capabilities” section and turn on “App Sandbox“. At this point with all the options of the sandbox expanded go in the “File Access” section and for the voice “User Selected File” set the permissions to “Read/Write”

Then open your Info.plist file and add the voice “CFBundleDocumentTypes” to it (if it is not already there), then within it add another voice called “CFBundleTypeExtensions” where you will list all the extensions that your related items may have and how you app can interact with them. Beware of two things in particular:

for the sub-voice “CFBundleTypeRole” be sure to set the value to “Editor” otherwise your app won’t be able to write related files

remember to add the NSIsRelatedItemType = TRUE voice

Vim

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

<key>CFBundleDocumentTypes</key>

<array>

<dict>

<key>CFBundleTypeExtensions</key>

<array>

<string>sub</string>

<string>srt</string>

<string>txt</string>

</array>

<key>CFBundleTypeName</key>

<string>SRT File</string>

<key>CFBundleTypeRole</key>

<string>Editor</string>

<key>NSIsRelatedItemType</key>

<true/>

</dict>

</array>

Now… if you correctly did everything stated before the configuration part is done and the coding can begin!

In order to write a related item file when the sandboxing is active you have to use an instance of the NSFileCoordinator class to which you have to provide an instance of a class implementing the NSFilePresenter protocol.

presentedItemUrl: this property has to return the path of the “related item” file (if the file still doesn’t exist it will return the path where it will be written)… in my case the path of the subtitles file

primaryPresentedItemUrl: this property has to return the path of the main file… in my case the path of the video file

presentedItemOperationQueue: this property returns an NSOperationQueue on which perform the tasks file related

Let’s see at how I implemented the NSFilePresenter protocol in the demo project:

Now after the class implementing the NSFilePresenter protocol has been instantiated you have to tell the system that you intend perform stuff on this file and hence it has to be ready about it… this can be accomplished calling the class method “addFilePresenter(filePresenter: NSFilePresenter)” of the class NSFileCoordinator.

Swift

1

2

3

4

5

6

// Using the FileData class showed before here it is how to register

// it to the NSFileCoordinator class

letpath="the path of a file... ex. /something/file.ext"

fileData=FileData(path:path)

NSFileCoordinator.addFilePresenter(self.fileData!)

Now to actually perform the operation on the file (either reading or writing) you have to create an instance of the class NSFileCoordinator and call one of its “coordinatesomething” methods. These methods accept in input a closure performing the task on the file. In the following example I will call the method “coordinateWritingItemAtURL” providing in input the url of the related item file, an options parameter (actually the parameter saying “no options”), the error variable and the closure that will actually write the file ( writing the string “Stuff to write in the file” inside the related item file ):

Swift

1

2

3

4

5

6

7

8

9

ifletfData=fileData,leturl=fData.presentedItemURL{

varerrorMain:NSError?

letcoord=NSFileCoordinator(filePresenter:fData)

coord.coordinateWritingItemAtURL(url,options:NSFileCoordinatorWritingOptions.allZeros,error:&errorMain,byAccessor:{writeUrl in

varerror:NSError?

"Stuff to write in the file".writeToFile(writeUrl.path!,atomically:true,encoding:NSUTF8StringEncoding,error:&error)

return

})

}

And we are done! This is all you need to know to be able to write files with same file name but a different extension of files you opened using NSOpenPanel/drag&drop in a sandboxed app.