Mac OS X's Preferences System (and More!)
Pages: 1, 2, 3
User Defaults
As an introduction to our next topic of discussion we're going to talk about another way we can save the address book data using OS X's preferences system. As you are undoubtedly aware of by now, Mac OS X is a multi-user system to its core, unlike OS 9. As such, each user has their own set of preferences for each application he uses, and Cocoa makes accessing and managing user preferences a painless task with the NSUserDefaults class.
NSUserDefaults provides a programmatic interface to Mac OS X's preferences system. Defaults are stored in a user's preferences database as key-value pairs in a data dictionary. Thus, working with preferences is fundamentally no different than how we learned to work with data dictionaries (NSDictionary). Preference values are stored using the NSUserDefaults method setObject:forKey: (among others), and you can later access defaults using the objectForKey: method.
To create an NSUserDefaults object, you send the class object a standardUserDefaults message, which returns the object we use to interact with the preferences database for the current user of application. Let's modify our code so that the records array is initialized from the user's defaults database, rather than from an arbitrary file. To start this we have to declare an NSUserDefaults instance variable in Controller.h:
NSUserDefaults *prefs;
Simple enough. The next step is to go back to awakeFromNib and do something analogous to what we did above initializing records from a file. We have to first initialize prefs to the defaults object using the standardUserDefaults method (making sure we retain it so it's available for future use):
prefs = [[NSUserDefaults standardUserDefaults] retain];
We then retrieve the stored array from prefs using objectForKey:. The key we use can be any string you want; we're going to use the string @"Addresses" here. So we send to prefs the message objectForKey:@"Addresses" and use the return value of that as the argument to initWithArray: (another NSMutableArray initializer method), and thus we have a stocked and ready to use mutable array:
records = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"Addresses"]];
Again we have the possibility that there may not exist a value for the key @"Addresses", in which case objectForKey: returns nil.
This situation gets handled a bit differently than before. You see, when we used initWithContentsOfFile:, the method returned nil if the file could not be loaded, which provided a convenient means of testing whether our array had been initialized successfully. However, we're using the NSUserDefaults method objectForKey, in conjunction with the NSArray initializer initWithArray: to initialize records. If objectForKey cannot find a value for the specified key, nil is returned. Using nil as the argument to initWithArray: will initialize records to an empty array, rather than returning nil. So we can't use the test nil == records. Rather, we're going to invert our if-statement logic to say that if there is a key @" Addresses", initialize records from that preference, or else initialize records using init. To do this test in the if-statement we use the same NSUserDefaults method objectForKey:, but we're not interested in storing the return value, we're just interested in seeing if a value is returned. In code this looks like the following:
- (void)awakeFromNib
{
prefs = [[NSUserDefaults standardUserDefaults] retain];
if ( [prefs objectForKey:@"Adresses"] != nil ) {
records = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"Addresses"]];
} else {
records = [[NSMutableArray alloc] init];
}
In general, to retrieve a default value from prefs, all we do is send it an objectForKey: message with the key corresponding to the particular preference we want to retrieve. However, NSUserDefaults defines several other methods such as arrayForKey:, boolForKey:, integerForKey:, and so on that allows us to be more specific as to what return type we expect.
The key difference here from objectForKey: is that objectForKey: will return nil if the specified key does not exist in the defaults database. arrayForKey:, on the other hand, goes a step further by returning nil if the object for the specified key is anything other than an array. The same is true for the other NSUserDefaults accessor methods and their respective return types.
Since we know for sure that the default we are using is going to be an array, we can use arrayForKey: rather than objectForKey:. This would change the relevant code to read in the second line of awakeFromNib:
records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];
The next step is to modify saveData to work with the defaults system. To store preferences to the database we use the NSUserDefaults method setObject: forKey:. We'll then get rid of the line in saveData [records writeToFile:recordsFile atomically:YES] and replace it with the code below:
- (void)saveData
{
[prefs setObject:records forKey:@"Addresses"];
}
One last thing we have to do is change our application's bundle identifier, which is what NSUserDefaults uses to identify the file where a user's preferences are stored. To set this name click on the "Targets" tab in Project Builder and click on the target "AddressBook." When the view in the editor window changes to the target options, click on the "Applications" tab. In this view under Basic Information you will the field "Identifier." This is where you set the bundle identifier name.
This name can be anything you desire. Standard practice, however, is to use the naming scheme of Java packages to help avoid naming conflicts between different applications. For those not familiar with this naming scheme, it's basically an Internet address in reverse that contains information about the company's name and the name of the application. For example, preferences for the Dock are stored under the name com.apple.dock, Terminal is under com.apple.terminal and so on. Our application will follow this convention and take the name com.YourName/Company.AddressBook. My version uses as its identifier com.mikebeam.AddressBook.
Now, when the preferences are actually stored to the disk, they are written to the user's Library/Preferences/ directory with the file name equal to the bundle identifier with a plist extension. Thus, my preferences for AddressBook will be found at /Users/mike/Library/Preferences/com.mikebeam.AddressBook.plist.
Apple provides a useful tool to view and edit the contents of property list documents. It is the application PropertyListEditor, found under /Developer/Applications. It might be instructive to open the files created by AddressBook in the different implementations shown in this column and see how property lists are structured. If you want to get a look at the nitty-gritty XML tags, you can open plist files in any text editor, giving you even more insight into how property lists are structured.
dealloc
Before leaving for the day we're going to tie up another loose end in AddressBook by implementing the dealloc method. The dealloc method is defined in NSObject and this message is sent to classes that are about to be released, or de-allocated -- basically it serves the opposite purpose of alloc.
We override dealloc in our classes to prepare it for being released. One of the things commonly done with dealloc is to release any object-instance variables that we allocated (and subsequently own). For example, in Controller we have the instance variables records, recordsFile and prefs. So in our implementation of dealloc we would send release messages to each of these objects to counter each alloc or retain message we may have previously sent to them. So it's our responsibility to free up their memory when they are no longer needed, which is surely the case when the object that references them is being destroyed. With this, here is our dealloc to be added to Controller.m:
- (void)dealloc
{
[self saveData]; // just to be sure that the
// latest data is saved to disk
[prefs synchronize];
[prefs release];
[records release];
[recordsFile release];
recordsFile = nil;
records = nil;
prefs = nil;
[super dealloc]; // pass the ball to Controller's
// superclass so it can do its own deallocation
}
What synchronize does is force the system to synchronize the defaults information contained in memory with what's stored on disk. Normally we don't manually use synchronize, as the system will do this automatically every so often. However, one time when we might want to force a synchronization is just before the application quits, as the system may not get a chance to do its auto-synchronization before the application closes. So that's what we're doing here.
Note that we didn't do anything with the outlet instance variables that we have. That's because we don't own those objects. We didn't assert ownership over them using alloc or retain, so we don't have to worry about them.
You may think dealloc is redundant in Controller since the memory occupied by all of the objects we released is freed when the app quits. However, say you want to use the Controller class here as part of some other application. This other application might not load Controller when the application launches as is done here, and the application might decide to release Controller sometime before the application quits.
In that case it would not be redundant to have a dealloc method, and by coding it right the first time, you save yourself some potentially time consuming work later. Besides, it's not that difficult or time consuming to implement dealloc like we've done here. So the moral of the story is to override the dealloc method whenever you have objects referenced by instance variables to clean up.
The End
I want to say a word about the two data-saving implementations we came up with today. Specifically, I want to address the question, "Which one is better? A separate file, or to store the data in the user defaults database?".
The answer to this is that user preferences are supposed to store non-critical data. That is, data that is not sorely missed if it is corrupted or lost. In our situation, it might be better to store the address book data in a separate file from user defaults, as that is something that you don't want to lose if you lose the preferences file.
But I guess one file is no safer than another file, so maybe the solution is to use both, using user defaults as the primary store house and then writing a separate file for backup. In awakeFromNib, if initializing records from prefs fails, then we can go to the backup file; if that fails, then we just initialize records to an empty array. Something like this:
if ( [prefs arrayForKey:@"Addresses"] != nil ) {
records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];
} else {
records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
if ( nil == records )
records = [[NSMutableArray alloc] init];
}
The project folder for this column can be downloaded here. This downloadable version of Address Book is set up to store data in both user defaults and a separate file as a backup, like we just mentioned. Additionally, I've shown how to set up the table data source methods to allow you to directly modify the table data from within the table that I briefly mentioned in the previous column.
With the basics of using NSUserDefaults under our belt, next time we'll more fully leverage the preferences system, as well as learn how to work with multiple windows in an application (i.e. a Preferences window). See you next time!
Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.
Read more Programming With Cocoa columns.
Return to the Mac DevCenter.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 26 of 26.
-
issue with automatically hide scrollers
2005-02-06 18:04:52 cmyk [Reply | View]
prefs = [[NSUserDefaults standardUserDefaults] retain];
if ( [prefs objectForKey:@"Adress"] != nil ) {
records = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:@"Adress"]];
[tableView reloadData]; //if not reloaded, automatically hiding scrollers won't show if they should
} else {
records = [[NSMutableArray alloc] init];
}
-
newbie on Xcode 1.2
2004-08-06 09:55:04 doublewood [Reply | View]
I am trying to follow this tutorial on Xcode 1.2 so far so good but this last tutorial got me. Its the last part of the tutorial where the records are save into preference of user. It does not generate the new preference .plist after I set up the identifier
-
Accessing System Preferences
2003-11-26 01:58:10 anonymous2 [Reply | View]
How to access System Preferences?
For example, I'd like to retrieve the currency or the date format in OS X preference pane...
Any idea?
Nicolas
-
Java Version - Solution
2002-05-26 17:49:04 jsumnertx [Reply | View]
I've been following these examples and doing them in Java rather than Objective C. Here is the solution:
Notes:
1) This project came in two parts. The first involved writing the data to a file specified by the user via the initWithContentsOfFile method of NSArray. However, this method doesn't exist in the Java version of NSArray and the documentation didn't say anything about how to do it. I searched around and found some anecdotal information but no actual code. Therefore, I'll show you my final code for the first part since it was a real pain to get working....
Read Prefs:
String recordsFile;
Controller()
{
recordsFile = "~/Library/Preferences/AddressBookData.plist";
recordsFile = NSPathUtilities.stringByExpandingTildeInPath(recordsFile);
try{
java.io.File file = new java.io.File(recordsFile);
NSData data = new NSData(file);
NSArray loadedRecords = (NSArray)NSPropertyListSerialization.propertyListFromData(data);
records = new NSMutableArray(loadedRecords);
}
catch (Exception e)
{
records = new NSMutableArray();
}
}
private void saveData(NSArray records)
{
try
{
java.net.URL fileURL=NSPathUtilities.URLWithPath(recordsFile);
NSData data = NSPropertyListSerialization.dataFromPropertyList(records);
data.writeToURL(fileURL, true);
}
catch (Exception e)
{
// put up a dialog
int status = NSAlertPanel.runAlert("Warning!", "Unable to save address data", "OK", null, null);
}
}
Section 2: Completed program using NSUserDefaults...
/* Controller */
import com.apple.cocoa.foundation.*;
import com.apple.cocoa.application.*;
public class Controller{
NSTextField emailField; /* IBOutlet */
NSTextField firstNameField; /* IBOutlet */
NSTextField homePhoneField; /* IBOutlet */
NSTextField lastNameField; /* IBOutlet */
NSTableView tableView; /* IBOutlet */
NSMutableArray records;
NSUserDefaults prefs;
//String recordsFile;
Controller()
{
prefs = NSUserDefaults.standardUserDefaults();
records = new NSMutableArray(prefs.arrayForKey("Address"));
}
public NSDictionary createRecord()
{
NSMutableDictionary record = new NSMutableDictionary();
record.setObjectForKey(firstNameField.stringValue(), "First Name");
record.setObjectForKey(lastNameField.stringValue(), "Last Name");
record.setObjectForKey(emailField.stringValue(), "Email");
record.setObjectForKey(homePhoneField.stringValue(), "Home Phone");
return record;
}
public void addRecord(Object sender) { /* IBAction */
records.addObject(createRecord());
tableView.reloadData();
saveData(records);
}
public void deleteRecord(Object sender) { /* IBAction */
if (tableView.numberOfSelectedRows() == 0)
return;
NSApplication.beep();
int status = NSAlertPanel.runAlert("Warning!", "Are you sure you want to delete the selected record(s)?", "OK", "Cancel", null);
if ( status == NSAlertPanel.DefaultReturn ) {
NSMutableArray tempArray = new NSMutableArray();
NSEnumerator enumerator = tableView.selectedRowEnumerator();
Integer item;
item = (Integer)enumerator.nextElement();
while ( item != null ) {
tempArray.addObject(records.objectAtIndex(item.intValue()));
item = (Integer)enumerator.nextElement();
}
records.removeObjectsInArray(tempArray);
tableView.reloadData();
saveData(records);
}
}
public void insertRecord(Object sender) { /* IBAction */
int index = tableView.selectedRow();
if (index >= 0)
records.insertObjectAtIndex(createRecord(), index);
else
records.addObject(createRecord());
tableView.reloadData();
saveData(records);
}
public int numberOfRowsInTableView(NSTableView aTableView){
return records.count();
}
public Object tableViewObjectValueForLocation( NSTableView aTableView, NSTableColumn aTableColumn, int rowIndex){
NSDictionary theRecord;
Object theValue=null;
theRecord = (NSDictionary)records.objectAtIndex(rowIndex);
if (theRecord!=null)
theValue = theRecord.objectForKey(aTableColumn.identifier());
return theValue;
}
private void saveData(NSArray records)
{
prefs.setObjectForKey(records, "Address");
}
}
-
Sorting?
2001-09-14 22:20:55 n9yty [Reply | View]
I've been trying to figure out (with no luck) how to get the tableView to sort....
I looked around and found a reference in setDoubleAction:(SEL) to set a selector used when a column header is double clicked... But how do I tie that to my code?
Further, I see clickedColumn in tableView, but it returns an index... Since you can re-arrange the columns, how do you know which one is which at runtime? I thought about something like using tableColumns to get the columns, then objectAtIndex:colnum to get the column clicked in, and finally the identifier method of this to get the column name.
Is this the wrong path? Any insight, please? :-)
Thanks for a great series of articles. I gave up most of my C programming back when I switched from my Amiga (Intuition was a nice environ for it's time, IMHO) to Windows (VisualBasic, but let's not go there...), and since moving to the Mac I've never gotten into programming, but Cocoa and MacOS X are really intriguing me... Most of my day-to-day work is in Perl/Shell/SQL/C for various Unix "back-end" things.
-
Sorting?
2001-09-15 06:08:57 n9yty [Reply | View]
Oh, forgot something...
I figured that once I had the identifier tag for the clicked-column, I could simply call the records sortUsingFunction, with a function that uses caseInsensitiveCompare on the values in the given column, which will be plucked out by the identifier name, passed into the function via the context: parameter.
[records sortUsingFunction:stringSort context:identifier]
and stringSort is basically something like:
NSString *v1 = [val1 objectForKey:context];
NSString *v2 = [val2 objectForKey:context];
return [v1 caseInsensitiveCompare:v2];
Is this really off track?
Thanks again!
-
Sorting - Sorting it out...
2001-09-15 20:21:36 n9yty [Reply | View]
Okay, found my problem with using the doubleAction to set the selector, I didn't know I had to setTarget first... Got that done, and now everything else has fallen into place and it seems to work great.... Hmm... Instead of sorting based on column clicked on, though, I'm wondering about a sort button that sorts based on the order you have columns positioned? Food for thought, but it's working as-is for now.
-
Leap of faith?
2001-09-01 15:03:02 TheBum [Reply | View]
Mike,
In the "Loading Data" section of the article, you describe the whole bit about creating an NSString with a convenience constructor and then creating a second, tilde-expanded NSString with an instance method for the file name. Farther down the page where you add the error checking, that first NSString creation is all of a sudden replaced by an @"..." string. What's up with that?
BTW, great series of articles!! I look forward to each one. -
Leap of faith?
2001-09-01 15:22:00 Michael Beam |
[Reply | View]
Hey,
That string being created statically using @"..." rather than the convenience constructor was probably just a slip-up when i was writing that part. In any event, either one works, they just work in different ways. Practically speaking, you're probably not increasing your performance/efficiency/whatever mush by using the convenience constructor, since its just one string. Its when you repeatedly create/destroy objects that you _really_ need to watch out for mem management issues. I did it that way just to point out another situation of how you can work memory management. Well, i'm off to see UT play New Mexico. I'm glad you're enjoying the columns!!
Mike
-
tests for nil in awakeFromNib incorrect
2001-08-29 19:55:01 jeremym [Reply | View]
Hi,
The test "if ( nil == records )" within the awakeFromNib method do not work. You use
records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];
To read the data from the preferences system into the data structure. Once the alloc is performed the pointer "records" no longer points to nil, regardless of what happened in the init code. You can test this by pointing a break point in the code, or like I did on accident. Run the program in the final form, add some stuff, and quit. Then delete the com.###.AddressBook.plist, but keep the AdressBookData.plist. Re-run the program and nothing shows up. The if statement returns NO, because your pointer has a value after alloc.
Here is what I used to solve the problem:
records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];
if ( nil == [records count]) {
records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
if ( nil == [records count] ) {
records = [[NSMutableArray alloc] init];
} else {
[self saveData];
}
}
}
The else { [self saveData] } serves to re create the preferences file from the AddressBookData.plist file if it is missing.
Great articles, I am learning a lot.
Jeremy -
tests for nil in awakeFromNib incorrect
2001-08-31 18:56:33 Michael Beam |
[Reply | View]
Indeed, records wouldn't be nil after doing [NSMutableArray alloc]. Thanks for catching that!
Now, in your fix wouldn't we want to do
if ( [records count] == 0 ) instead of
if ( [records count] == nil )? I'm trying to think if there's a more fundamental to test whether or not the initialization was successful, but can't think of anything at the moment.
The last [self saveData] isn't strictly necessary, as the preferences file would be re-written the first time a record is added. I was actually debating whether or not to do just this when i was writing this code, and I decided to keep things less cluttered, pedagogically speaking. But on the other hand, it's not a bad idea either.
Thinking about it a little more, we could have two different types of if statements, rather than the way we've been doing it. For example, we could do the following:
if ( nil != [prefs arrayForKey:@"Addresses"] ) {
records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"] )
} else if ( [[NSFileManager defaultFileManager] fileExistsAtPath:recordsFile] == YES ) {
records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
} else {
records = [[NSMutableArray alloc] init];
}
(I hope this code posts right, tough in this small editing window :) -
tests for nil in awakeFromNib incorrect
2001-08-31 19:04:10 Michael Beam |
[Reply | View]
Okay, i posted before i finished what i wanted to say: So in the first if statement, we test to see if the key exists in the user's prefs, if so, then we go ahead and use prefs to initialize records, if not we move onto the next if statement. In if-number-two we test to see if the file recordsFile exists at the path, using the NSFileManager method fileExistsAtPath:, which returns a BOOL. If the return value is YES, then we initialize records from the file on disk. if now, then we initialize to an empty array. Again, if you want you can put [self saveData] in the code block for the second if statement and the else code block. As you can see this is only one way to do it. If someone comes up with another way, share it with us!
Mike -
tests for nil in awakeFromNib incorrect
2003-05-18 20:33:11 anonymous2 [Reply | View]
It is strange that according to my test, if an init... operation failed, it does return a nil. I read the manual from Apple, it also says that a failed init... operation will release the object and return a nil. So, I think your original code is correct. :-). But I agree that you should add a [self saveData]; statement in the awakeFromNib method. Coz, it seems that the [self saveData] in the dealloc can not really save the data, so you will not re-create the missing file is you haven't modified the data. Strange!
-
Missing Retain
2001-08-25 11:54:48 johnts [Reply | View]
I entered the changes (without the preferences part), and when I ran it when i clicked Add, all I got was the spinning CD of death. I killed it and noticed that the final awakeFromNib: method on the first page didn't have the [recordsFile retain]. I added it and it works now.
The strange part is, I can't find the saved file! I look in my ~/Library/Preferences folder, but it's not there! When I re-run, the values are loaded, just have to find out from where! -
Missing Retain
2001-08-25 15:44:56 Michael Beam |
[Reply | View]
Thanks for catching that one! It'll get fixed on Monday too. That's weird that you don't see the file. Did you download the project i posted with the article and see if you could find the file it produces? BTW, are we talking about the file from the first part of the article,or the file created by NSUSerDefaults?
Mike -
Missing Retain
2001-08-25 22:04:35 johnts [Reply | View]
I don't know what happened, but the file (the one in ~/Library/Preferences) just appeared. It's there now, but I don't know why it took a little while for it to appear. I even went into the Terminal and looked for it, just in case the Finder was just not updating. -
Missing Retain
2001-08-26 01:25:32 Michael Beam |
[Reply | View]
Huh. That's bizarre. Glad things are working for you now...
Mike -
Missing Retain -- Original Code Correct; Published Code Mangled in Production
2001-08-27 11:11:25 Derrick Story |
[Reply | View]
I just went through Mike's original document and compared it to what's published. Mike's original code was correct, and we managed to mangle it during our production process. We'll have it fixed up again by Monday aft. Everything should run smoothly after that. We'll also take a peek at what happened. Sorry about that folks! -
Missing Retain -- All Clean
2001-08-27 14:16:29 Derrick Story |
[Reply | View]
OK, I think I have everything nice and tidy.
-
Extraneous text in the sample code
2001-08-24 22:54:41 mjw [Reply | View]
I reloaded the page and it continued to come up: You don't mean to have extra text like "/mac/2001/08/24/" and "nbsp;" in the sample code, do you? It shows up on page 1 only. -
Extraneous text in the sample code
2001-08-25 11:40:11 Michael Beam |
[Reply | View]
Yeah, i submitted that Friday afternoon after i got my preview copy, it should get fixed on monday. Thanks for keeping us on our toes!
Mike -
Extraneous text in the sample code -- Repair underway by editorial staff
2001-08-27 11:12:33 Derrick Story |
[Reply | View]
Same as previous note: I just went through Mike's original document and compared it to what's published. Mike's original code was correct, and we managed to mangle it during our production process. We'll have it fixed up again by Monday aft. Everything should run smoothly after that. We'll also take a peek at what happened. Sorry about that folks!






I was working on the Addressbook application.
What i want to do is,
I want to add the contents of NSTextView into theNSDictionary.
As NSTextField cannot hold multiple lines of information.
How I can implement this.