iosdev

Never save absolute file paths in your iOS app

There’s a mistake I make every 6 months or so and I hope writing a blog post about it will finally engrave it into the back of my mind.

Have you seen how the file paths look like for the files in your iOS app’s little sandbox? Something like this:

/var/mobile/Applications/F71BA910-A1F0-4B39-85CB-775806ACFF62/Documents/orders/1832006/1.pdf

The mistake I make is that I take this full URL and then save it into to Core Data storage or wherever (in this case, as order file path). And this will work if you never make updates to the app. However, if you do publish an update to the app, this URL will not be valid anymore.

The gibberish bit in the middle - F71BA910-A1F0-4B39-85CB-775806ACFF62 - is path that is specific to a given app version. So, when you update, your new app version will get some other string in there and all your previously saved / hard-coded URLs will be useless.

What you need to save is only the part that you control and maintain - in my case that’s orders/1832006/1.pdf. Which excludes the Documents part as well as this is automatically created by iOS for your app. Documents is one of several app directories that you can and should fetch using the provided iOS APIs. There is nothing that guaranties that in some future iOS versions Apple won’t rename Documents to something else, so you should not think it’s there to stay.

So, what’s the proper way to read/save local path files?

Apple already provides few useful functions:

//	get absolute path for your app
NSString *NSHomeDirectory()
//	get absolute path for your app's temporary (tmp) folder
NSString *NSTemporaryDirectory()

Along with this, I’ve long been using a helper NSFileManager methods that Erica Sadun wrote way back in iPhone OS 3.0 time, for her iPhone Developer Cookbook. I have updated it over time and the current version that I use is in my Github repository. In it, you will find more appropriately named methods:

//	this gives you an absolute path to YOUR_APP/Documents directory
NSString *NSDocumentsFolder();
//	this gives you an absolute path to YOUR_APP/Library directory
NSString *NSLibraryFolder();

Using Folder in the names instead of Directory is deliberate, to avoid potential App Store rejections due to use of private methods that Apple may be/is using.

This small extension also have few more very useful methods:

+ (BOOL)findOrCreateDirectoryPath:(NSString *)path;
+ (BOOL)findOrCreateDirectoryPath:(NSString *)path backup:(BOOL)shouldBackup dataProtection:(NSString *)dataProtection;

First one is simply a shortcut for the second with default values. So, what this does is - you supply an absolute folder path and it will return TRUE if it has found it (and it is a directory) or has successfully created it. In any other case, it will return FALSE.

Additionally, if you want to exclude this folder from iCloud/iTunes backups, then send NO for the shouldBackup (default is YES). Lastly, if you want to use iOS Data Protection APIs, then supply the appropriate protection level constant. These last two actions are best-effort side-effects - what happens when trying to set them does not influence the method result. This is just the way it worked for me here, you can change this as you want/need.

For example, here’s a sample folder path function I added for one of the apps I created:

+ (NSString *)RTOrdersFolder {
	
	NSString *f = [NSDocumentsFolder() stringByAppendingPathComponent:@"orders"];
	if ([NSFileManager findOrCreateDirectoryPath:f shouldBackup:NO dataProtection:NSFileProtectionComplete])
		return f;
	
	return nil;
}

On first time this is called, it will create this folder inside the Documents folder, mark it so it does not go into iCloud (don’t ask) and also to fully protect it from prying eyes. Every subsequent access to this folder will simply return its absolute path.

How do I use this new method in the app? Here’s typical (simplified) usage (order is custom object in the app):

NSString *fullOrderPath = [[NSFileManager RTOrdersFolder] stringByAppendingPathComponent:order.orderId];

order.finalPath = [fullOrderPath stringByReplacingOccurrencesOfString:[NSFileManager RTOrdersFolder] withString:@""];

Thus when you persist the order object, only the relevant part of the path - the part that you have control over - is saved. When you need full path again, it’s easy to recreate it.

I recommend that you create methods like this for any file resources that you often reference in your app.