Accessing the *real* home folder from a sandboxed app

The preferred way to find a path for a directory within the user’s folder is to use NSSearchPathForDirectoriesInDomains (or an NSFileManager equivalent, such as URLsForDirectory:inDomains:). Problem with that is, if your app is sandboxed, these fuctions won’t give you paths the actual directories you’ve asked for, but rather the equivalent paths within your app’s container, even if you’re using entitlements which allow access to those paths.

So, let’s say you want to get the path for the user’s Documents directory. You’d end up with something like this:

- (NSURL*) getDocumentsDirectoryURL
{
    NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES );
    NSString* documentsPath = paths[0];
    return [NSURL fileURLWithPath:documentsPath];
}

You might expect to get back something like file://localhost/Volumes/SSD/Users/zach/Documents/ (at least, you’d expect that if your name was Zach). But you might be surprised when you actually get something more like file://localhost/Volumes/SSD/Users/zach/Library/Containers/com.mycompany.myapp/Data/Documents/.

If you really want the path to the real Documents directory (in ~/Users/zach/Documents), you need to do something like this:

- (NSURL*) getDocumentsDirectoryURL
{
    struct passwd *pw = getpwuid(getuid());
    NSString* realHomeDir = [NSString stringWithUTF8String:pw->pw_dir];
    NSString* documentsPath = [realHomeDir stringByAppendingPathComponent:@"Documents"];
    return [NSURL fileURLWithPath:documentsPath];
}

Admittedly, there aren’t many cases where you would want to do this. One reason you’d do this is to set the default location for an open/save file dialog – navigating the user to the Documents directory in your app container would be quite confusing. In my case, VideoBuffet really wants to find all the movies in your Movies folder, and there of course aren’t any in the Movies folder equivalent of the app container.

Notably, if you’re doing something like getting the path to Caches or Library (say, to save some settings into a .plist), you definitely want to use the normal NSSearchPathForDirectoriesInDomains method, because those should be saved into the app conainer.

Be careful with weak references

I’m working on an update to VideoBuffet. One of the changes is that movies are loaded on a background thread, to make the main UI responsive during loading. So I’ve a block of code in one of my view controllers that looks like this:

- (void) setCurrentPathURLs:(NSArray*)URLs
{
    [self moviesStartedLoading];

    __weak id weakSelf = self;
    [self.moviesDoc setMovieURLs:URLs completion:^{
        [weakSelf moviesFinishedLoading];
    }];
}

The little dance with a __weak reference is to avoid a retain cycle. The controller has a strong reference to moviesDoc, moviesDoc has a strong reference to the block, and because the block references self, it will have a strong reference to it. By making a weak reference to self, it won’t be retained by the block, and the cycle is broken.

I thought this was all good, until I did some sanity testing on 10.7 Lion, and discovered that it simply crashed when trying to open a movie. It didn’t take long to trace it back to the above code. Then I remembered this. Whoops, I’m making a weak reference to an NSViewController. Turns out that’s fine in 10.8, but not 10.7, according to the Transitioning to ARC Release Notes:

Note: In addition, in OS X v10.7, you cannot create weak references to
instances of NSFontManager, NSFontPanel, NSImage, NSTableCellView,
NSViewController, NSWindow, and NSWindowController. In addition, in
OS X v10.7 no classes in the AV Foundation framework support weak references.

The solution is to simply make it __unsafe_unretained, as in:

- (void) setCurrentPathURLs:(NSArray*)URLs
{
    [self moviesStartedLoading];

    __unsafe_unretained id weakSelf = self;
    [self.moviesDoc setMovieURLs:URLs completion:^{
        [weakSelf moviesFinishedLoading];
    }];
}

Really, though, the moral of the story is: test your app, on all OS’es it supports. If we hadn’t gone back and tested with 10.7, we’d never have discovered this, and we’d have ended up with many 1 star reviews.