Cocoa in the Shell

Saving and Sandboxing

As you know if you are a Mac developer sandboxing is mandatory since June, at least if you want your App to be in the Mac App Store, but perhaps you don’t. I’m not gonna discuss what is the concept of sandboxing because it has already been done many time.

The point is, many developers complained[1] about it because it alters the comportment of many programs and tends to complicate a lot the code, in some cases sandboxing is not even possible.

To illustrate this, just imagine you have an app that accepts an image by drag & drop.
When you drag an image on the app, a filter is applied to it and a new image is saved at the same location as the original or to a folder specified by the user in some preferences.
Pretty straightforward, easy to code, easy to use. Let’s take a look at the differences between the sandboxed version and the normal one.

Normal version

Let’s start from the point where we have our filtered image as a CGImageRef, and the destination path as a NSURL.
We create a CGImageDestinationRef and we are done.

CGImageDestinationRef dest = CGImageDestinationCreateWithURL((__bridge CFURLRef)url, kUTTypeJPEG, 1, NULL);
if (!dest)
    return;
CGImageDestinationAddImage(dest, img, NULL);
const bool ret = CGImageDestinationFinalize(dest);
CFRelease(dest);

2 scenarios :

  1. The user wants the filtered image saved to the same location as the original, assuming the URL is correct, it just works.
  2. The user wants to save the image in a specific folder, if the URL is correct it just works too.

With sandboxing

3 scenarios are possible :

  1. The user wants the filtered image saved to the same location as the original, we are fucked if the image comes from a directory that is not allowed in a sandboxed environment, we have to prompt the user where he wants to save the image each freaking time.
  2. The user wants to save the image in a specific directory and this folder is allowed in a sandboxed environment (Download, Movies, Music, Pictures), it will work the same as before assuming we specified the correct entitlement, for example com.apple.security.assets.pictures.read-write
  3. The user wants to save the image in a specific directory, but this directory is not allowed in a sandboxed env, like the desktop. We need to add two entitlements : com.apple.security.files.user-selected.read-write and com.apple.security.files.bookmarks.app-scope and use the Security-Scoped Bookmarks.

So let’s dive into the last point and look at the code. First we ask the user where he wants to save the images, like before we can use a NSOpenPanel, but we need to transform the URL returned to a Security-Scoped bookmark.

-(IBAction)selectSaveDirectory:(id)sender
{
    NSOpenPanel* panel = [NSOpenPanel openPanel];
    [panel setCanChooseDirectories:YES];
    [panel setCanChooseFiles:NO];
    [panel beginSheetModalForWindow:[self window] completionHandler:^(NSInteger result) {
        NSURL* url = [panel URL];
        NSData* data = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:nil];
        if (data)
        {
            NSUserDefaults* prefs = [NSUserDefaults standardUserDefaults];
            [prefs setObject:data forKey:@"ExportDirectory"];
            [prefs synchronize];
        }
    }];
}

Now that we have a Security-Scoped bookmark we can use it to save the image. First we need to transform the data back to a NSURL, then append it the filename. Once this is done, we explicitly specify that we want to access this URL to perform the save.

NSURL* outURL = [NSURL URLByResolvingBookmarkData:[[NSUserDefaults standardUserDefaults] objectForKey:@"ExportDirectory"] options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:nil error:nil];
if (!outURL)
{
    NSLog(@"Screw this fucking sandboxing thing.");
    return;
}
outURL = [outURL URLByAppendingPathComponent:outFilename];
[outURL startAccessingSecurityScopedResource];
CGImageDestinationRef dest = CGImageDestinationCreateWithURL((__bridge CFURLRef)outURL, kUTTypeJPEG, 1, NULL);
if (!dest)
{
    [outURL stopAccessingSecurityScopedResource];
    return;
}
CGImageDestinationAddImage(dest, img, NULL);
const bool ret = CGImageDestinationFinalize(dest);
CFRelease(dest);
[outURL stopAccessingSecurityScopedResource];

And if everything is fine the image will be saved where you expect.

So much boilerplate code to simply save a file, and the drag & drop feature is pretty much useless, coding used to be funnier.


  1. some people would say developers are always complaining, I myself whine quite a lot, but that’s just because I’m French.  ↩