1. An iOS Application’s Directory Structure
Persisting and archiving require writing data to a
file, but an iOS application can only read and write to files in the
application’s sandbox. When installed, an application is placed in its
own home directory. This directory is the application’s root directory
and should be left untouched, lest you risk corrupting your application.
Under the application’s home directory are the directories you may
write to. These directories are the Documents, Preferences, Caches, and
tmp directories.
<application home directory>/Documents
<application home directory>/Library/Preferences
<application home directory>/Library/Caches
<application home directory>/tmp
The Documents directory is where you should write
your application’s data files. The Preferences directory is where your
application’s preferences are stored. These are the preferences set
through the iOS’s Settings application, using the NSUserDefaults class,
not preferences you might create programmatically. The Caches directory,
like the Documents directory, is another location you can persist files
to, although, as the name implies, this directory should be reserved
for caching data rather than storing an application’s files. The tmp
directory is a temporary directory for writing files that do not need to
be persisted between application launches. Your application should
remove files from this directory when not needed and iOS also removes
files from this folder when an application is not running.
Directories
You will mostly read and write from two directories:
the Documents directory and the tmp directory. Files you want to persist
between application launches should go in the Documents directory.
These files are also backed up by iTunes when an iPhone, iPod touch, or
iPad is synchronized with a user’s computer. Files placed in the tmp
folder are temporary and should be deleted when an application
terminates. If the application does not clean the folder, iOS might
delete them depending on when space is needed on your device. You should
never hard-code
a path in your code to either folder. When using the Documents folder,
you should use the NSHomeDirectory method combined with the
NSSearchPathForDirectoriesInDomain method. When obtaining the tmp
directory, you should use the NSTemporaryDirectory method.
NSHomeDirectory
The NSHomeDirectory is how you should obtain an application’s root directory.
NSString * NSHomeDirectory (void);
Obtain the path to your Documents directory using the
NSHomeDirectory. By itself, this method isn’t very useful, as you
usually want to obtain the Documents directory.
NSSearchPathForDirectoriesInDomains
Obtain the path to your application’s Documents directory using the NSSearchPathFor DirectoriesInDomains.
NSArray * NSSearchPathForDirectoriesInDomains (
NSSearchPathDirectory directory,
NSSearchPathDomainMask domainMask, BOOL expandTilde );
The method takes three parameters: the directory to
begin the search, the search path domain mask, and a flag indicating if
tildes should be converted to actual paths. The method returns an array
of paths. Although on a desktop or laptop there might be multiple
elements in the array, on an iOS device, there will only be one result
in the array. The following code illustrates how to obtain an
application’s Documents directory on an iOS device:
NSArray * myPaths = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask,
YES); NSString * myDocPath = [myPaths objectAtIndex:0];
Values you might use for the directory parameter on
an iOS device include NSDocumentDirectory, NSApplicationDirectory,
NSCachesDirectory, and NSApplicationSupportDirectory.
NSTemporaryDirectory
The NSTemporaryDirectory method returns the path to your application’s tmp directory.
NSString * NSTemporaryDirectory (void);
Unlike the NSHomeDirectory method, the
NSTemporaryDirectory method is useful by itself, as it is the most
direct way to obtain a path to your application’s tmp directory.
2. Property Lists
The easiest way to save your application’s
preferences if you’re managing them within your application is using a
property list. If an object can be serialized, you can persist it to a
file using a path or URL. You can also reconstitute the object by
reading it from the file.
It
is worth noting that only objects can be serialized. A common source of
frustration is trying to serialize a primitive int. Since primitive
data types are not serializable, they need to be converted to NSObjects
(e.g., int to NSNumber).
Simple Serialization
The NSDictionary, NSArray, NSString, NSNumber, and
NSData classes, and their mutable equivalents, can all be saved as a
property list using the writeToFile: or writeToURL: method.
-(BOOL)writeToFile:(NSString *) path atomically:(BOOL)flag
-(BOOL)writeToURL:(NSURL *) aURL atomically:(BOOL)flag
The first parameter is the path, or URL, to save the
file as. The second parameter is a flag indicating if the file should
first be saved to an auxiliary file. If the flag is YES, the data is
written to an auxiliary file that is then renamed to the file indicated
by the path or URL. Writing to an auxiliary file prevents the file
system from becoming corrupt should writing the file fail midstream.
Note
You can refer to the NSDictionary, NSArray, NSString,
NSNumber, or NSData classes, or one of their mutable equivalents, as a
property list object. So you could say “the property list objects all
contain . . .” rather than naming each property list object
individually.
Reading a property list back into the object uses the initWithContentsOfFile: or initWithContentsOfURL: method.
-(id)initWithContentsOfFile:(NSString *)path
-(id)initWithContentsOfURL:(NSURL *)aURL
The initWithContentsOfFile: method takes a path to
the property file, while the initWithContentsOfURL: takes a URL. Both
return an id.
Create a new View-based Application named SimpleArray. Open SimpleArrayAppDelegate.m and modify applicationDidFinishLaunching WithOptions to match Listing 1. After running the application, navigate to properties.plist in the file system and open it using TextEdit (Listing 3).
|
Listing 1. The applicationDidFinishLaunching method in SimpleArrayAppDelegate.m
(BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSMutableArray * dataArray = [[NSMutableArray alloc]
initWithObjects: @"First", @"Second", @"Third", nil];
NSString * path = [(NSString *) [NSSearchPathForDirectoriesInDomains
(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]
stringByAppendingPathComponent:@"properties.plist"];
[dataArray writeToFile:path atomically:YES];
NSArray * dataArray2 = [[NSArray alloc] initWithContentsOfFile:path];
NSLog(@"objects: %@, %@, %@",
[dataArray2 objectAtIndex:0], [dataArray2 objectAtIndex:1],
[dataArray2 objectAtIndex:2]);
[window addSubview:viewController.view];
[window makeKeyAndVisible];
[dataArray release];
[dataArray2 release];
}
|
Listing 2. Logging to the Debugger Console
2010-09-11 11:17:41.591 SimpleArray[14500:207] objects: First, Second,
Third
|
Listing 3. The properties.plist file is saved as XML.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<string>First</string>
<string>Second</string>
<string>Third</string>
</array>
</plist>
|
The
application first gets a path to its Documents directory and adds the
filename to the path. After creating the path, the application persists
the array to a file. Immediately after persisting the file, it creates a
new array from the file’s content and logs the array’s values to the
Debugger Console.
One thing interesting to note is that the application
persists the array in an XML format. If you wished, you could easily
modify the data in any text editor. You could also persist the
application to a URL, and since it is XML with a published document type
definition (DTD), you could process the file with almost any back-end
programming language that had libraries for parsing XML and DTD files.
However, note that the writeToURL:atomically: method is synchronous and
your application will halt processing until the data is successfully
written to the URL, so you are better off using the NSURLConnection
class so that your application doesn’t appear to freeze up until all of
the data has been written.
NSPropertyListSerialization
Using the writeToFile: method to save a property list
object as a simple property list is usually sufficient, but another way
you can persist a property list object to a property list is by using
the NSPropertyListSerialization class.
Serializing
To serialize a property list object, use the dataFromPropertyList:format:errorDescription: method.
+(NSData *)dataFromPropertyList:(id)plist format:
(NSPropertyListFormat *)format
errorDescription:(NSString **) errorString
This method’s first parameter is an id that
references the property list data and must be a property list object.
Note that the dataFromPropertyList:format:errorDescription: method
doesn’t open and read a file’s content; you must first obtain the data
using the initWithContentsOfFile: or initWithContentsOfURL: method. The
method’s second parameter is the property list’s desired format. This
parameter is one of the valid NSPropertyListFormat types:
NSPropertyListOpenStepFormat, NSPropertyListXMLFormat_ v1_0, or
NSPropertyListBinaryFormat_v1_0. The method’s final parameter is a
string to place an error description should something fail. Note, you
must release this string should an error occur. The method returns an
NSData object. You can then write this object to disk, using the
writeToFile: or writeToURL: method.
Deserializing
To deserialize a property list, use the propertyListFromData:mutabilityOption:format: errorDescription: method.
+ (id)propertyListFromData:(NSData *)data
mutabilityOption: (NSPropertyListMutabilityOptions) opt
format: (NSPropertyListFormat *)format
errorDescription:(NSString **) errorString
This
method’s first parameter is the data to deserialize. The method’s
second parameter indicates if the properties should be immutable or
mutable. The method’s third parameter indicates the format to make the
property list, and the fourth parameter is the error description. Valid
values for the second parameter are NSPropertyListImmutable,
NSPropertyListMutableContainers, and
NSPropertyListMutableContainersAndLeaves. Valid values for the third
parameter are NSPropertyListOpenStepFormat,
NSPropertyListXMLFormat_v1_0, and NSPropertyListBinary Format_v1_0. Note
that as with the dataFromPropertyList: method, should something fail,
you must release the NSString holding the error description.
Note
Do not take this task’s more complex data structure
as implying you cannot use a property list object’s writeToFile: or
writeToURL: method to persist complex data structures. You can, provided
all items in a data structure are a property list object. For instance,
if an NSArray’s elements each contained an NSDictionary, you could
serialize the entire data structure at once by writing the NSArray to a
file.
Create a new View-based Application named Properties. Open PropertiesAppDelegate.m and modify the applicationDidFinishLaunching WithOptions method (Listing 4).
|
Listing 4. The PropertiesAppDelegate’s applicationDidFinishLaunchingWithOptions method
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSString * errorDescription;
NSString *pathToFile = [[NSSearchPathForDirectoriesInDomains
(NSDocumentDirectory, NSUserDomainMask,YES) objectAtIndex:0]
stringByAppendingPathComponent:@"properties.plist"];
NSData * myData;
NSLog(@"%@", pathToFile);
if ([[NSFileManager defaultManager] fileExistsAtPath:pathToFile] == NO) {
NSMutableDictionary * dict2Serialize =
[[[NSMutableDictionary alloc] init] autorelease];
NSString * name = @"James";
NSArray * kids = [NSArray arrayWithObjects:
@"Nicolas", @"Juliana", nil];
NSNumber * age = [NSNumber numberWithInt:40];
[dict2Serialize setObject:name forKey:@"name"];
[dict2Serialize setObject:kids forKey:@"kids"];
[dict2Serialize setObject:age forKey:@"age"];
myData = [NSPropertyListSerialization dataFromPropertyList:(id)
dict2Serialize format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorDescription];
if (myData)
[myData writeToFile:pathToFile atomically:YES];
else {
NSLog(@"Error writing to myData, error: %@", errorDescription);
[errorDescription release];
}
}
else {
NSLog(@"property file exists....");
NSPropertyListFormat format;
NSData * plistData = [NSData dataWithContentsOfFile:pathToFile];
NSDictionary * props = (NSDictionary *)[NSPropertyListSerialization
propertyListFromData:plistData
mutabilityOption:NSPropertyListImmutable
format: &format errorDescription: &errorDescription];
if (props) {
NSLog(@"name: %@", [props objectForKey:@"name"]);
NSLog(@"age: %i",
[(NSNumber *)[props objectForKey:@"age"] intValue]);
NSLog(@"kid: %@", (NSString *)[(NSArray *)
[props objectForKey:@"kids"] objectAtIndex:0]);
NSLog(@"kid: %@", (NSString *)[(NSArray *)
[props objectForKey:@"kids"] objectAtIndex:1]);
} else {
NSLog(@"Error reading properties, error: %@", errorDescription);
[errorDescription release];
}
}
[window addSubview:viewController.view];
[window makeKeyAndVisible];
return YES;
}
|
The first time you run the application, the debugger
output will contain only a path. The second time, however, the
application logs the property list contents to the Debugger Console.
Notice that rather than writing the NSDictionary directly to disk, you
first transformed it into an NSData object representing the property
list. Had this first step of converting to XML gone awry, you would have
the error description informing you (hopefully) where the problem
occurred. This error handling is not provided using the
NSMutableDictionary’s writeToFile: method. After converting to a
property list, you then persisted it using the NSData’s writeToFile
method. Listing 5
lists the file’s XML content. Upon running the application a second
time, you read the property list as an NSData object and converted it to
an NSDictionary. To prove that the data was in fact reconstituted
correctly, you logged the output to the Debugger Console (Listing 6).
Listing 5. The application’s plist saved as XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www
.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>age</key>
<integer>40</integer>
<key>kids</key>
<array>
<string>Nicolas</string>
<string>Juliana</string>
</array>
<key>name</key>
<string>James</string>
</dict> </plist>
|
Listing 6. The application’s Debugger Console logging
2010-09-11 11:35:07.918 Properties[14672:207] /Users/bward/Library/
Application Support/iPhone Simulator/4.1/Applications/3D6D7BC3-8957-
4F2A-977B-6016E86F28C4/Documents/properties.plist
2010-09-11 11:35:07.920 Properties[14672:207] property file exists....
2010-09-11 11:35:07.922 Properties[14672:207] name: James
2010-09-11 11:35:07.922 Properties[14672:207] age: 40
2010-09-11 11:35:07.924 Properties[14672:207] kid: Nicolas
2010-09-11 11:35:07.925 Properties[14672:207] kid: Juliana