Monday, March 22, 2010

Absolute Filenames

Well, I got bit today by something I should've known better than to do. It was a classic newbie mistake so, but it caused me enough grief that I feel it's worth embarrassing myself with a cautionary post. Here's what happened:

When you use Core Data, Apple recommends that except for relatively small chunks of binary data, you store binary data like images, sounds, and movies in the file system and not inside Core Data's persistent store. Now, in my experience, SQLite can handle even really large blobs without too much additional overhead, but when Apple makes a specific recommendation like this, I find it's usually a good idea to follow that recommendation unless I have a really good reason not to. In this case, I didn't.

In an app I'm working on for a client, I'm recording audio using AVAudioRecorder. I don't ask the user to choose a name for the file, I just store the recordings as caf files in the file system using a filename based on the date and time, like so:
NSString *audioPathString = [NSString stringWithFormat:@"%@/%@", DOCUMENTS_FOLDER, [item valueForKey:@"audioPath"]];


Then, later on, after the recording is done, I store audioPathString inside a managed object that represents audio recordings:

audioItem.audioPath = audioPathString;


Then, when I want to play the sound, I just pull the pathname from Core Data and load the data from the file system, sort of like this:

NSError *error;

NSURL *url = [NSURL fileURLWithPath:item.audioPath];
AVAudioPlayer *player =[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];


And it worked great. For a while. I could quit and start back up and it would keep working. And then, at some point, it wouldn't work any more. It would report back OSStatus codes of -43 or -50 when I tried to create an instance of AVAudioPlayer, even though the audio file was still in the /Documents directory. I'm sure a few of you are chuckling to yourselves about my dumb error, but for those who don't see it, I'll explain.

Tip: If you find yourself in a situation where an NSError gives you nothing more than an OSStatus or OSErr error code, you can find out what that error code means by dropping to the terminal and typing macerror followed by the error code you're interested in, including the - (these old-style error codes are negative numbers).


The full pathname to an item in the Documents directory of an application's sandbox includes not the name of the application, but a UUID (unique identifier) instead. When you build and run your application, a new UUID is calculated if, and only if, something has changed since your last build. If you haven't made any changes, it keeps running with the previous UUID. Once you make some changes to code or a resource and re-compile, the UUID changes, and the stored absolute path no longer points to the right location. The portion of the URL that represents the Application's UUID has now changed. Xcode is smart enough to move the application's /Documents directory to the new location so you don't lose all of your data while developing, but all that data is now at a new location in the file system.

So, how do you handle this situation? The easiest way is to just store the filename and then re-build the absolute path at runtime by assuming the file is in your application's /Documents directory. So, instead of storing the path from recorderFilePath as I did in the earlier code sample, you would store just the last path component using NSString's lastPathComponent method, like so:

newItem.audioPath = [recorderFilePath lastPathComponent];


Then, when it's time to load the file, you just re-build the full path to the file based on the current UUID of the application, like so:

NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *audioPathString = [NSString stringWithFormat:@"%@/%@", documentsDirectory, [item valueForKey:@"audioPath"]];


Using this approach, your code will continue to function even between development builds or application releases, and you won't waste an hour trying to figure out why your perfectly valid files won't load.

No comments:

Post a Comment