In this section, we’ll modify the GraphPaper application to work with the defaults database. We will do this by making changes to both the Controller and the ColorGraphView classes.
Cocoa applications use a single instance of the Foundation class NSUserDefaults for managing the defaults database. Apple’s documentation notes that the NSUserDefaults class is not thread-safe, so you should use it only from your application’s main thread.
To use the NSUserDefaults class, you must first decide what default values your application will need to store. Each of these values should be given a name that will be its key in the defaults database. For each value, you must also decide upon a representation — that is, how the representation will be stored. Table 21-3 shows the defaults that we will use for GraphPaper.
Table 21-3. Defaults for the GraphPaper application
Default name |
#define |
Our default |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
0.1 |
|
|
-10.0 |
|
|
10.0 |
|
|
-5.0 |
|
|
5.0 |
|
|
|
GraphPaper window frame |
Not needed |
|
Once you have decided on the default values that your application will be using, you need to write the code that will install these into the default registration table. This table will provide the default default values for your application — that is, the values that the application will use before any are set by the user.
Apple recommends that you register the defaults that each of your classes will use in a method you create called +initialize . The +initialize method is a special class method that is invoked when your class is first used (recall that the plus (+) means class method, whereas a minus (-) means instance method). The Objective-C runtime system ensures that the initialize message[40] is sent once and only once to each class in your program. The initialize message is always sent to a class’s superclass before it is sent to the class itself.
The GraphPaper application will use the defaults system in three locations:
The GraphView class will use the
defaults system to determine the initial values of the
xmin
, xmax
,
xstep
, ymin
,
ymax
, and formula
values,
overriding the information stored in the NSForm instance in
MainMenu.nib
.
The ColorGraphView class will use the defaults
system to determine the initial values of the
AxesColor
, LabelColor
, and
GraphColor
colors, overriding the values that were
hardcoded into the ColorGraphView class.
The NSWindow class will use the defaults system to determine the initial location of the GraphPaper window.
For consistency and to prevent typographical errors, we will create a
separate file called
defaults.h
that will contain
#define
values for each of the default keys.
Create the defaults.h
file (in PB or elsewhere)
and save it in your ~/GraphPaper
folder.
// defaults.h // Define the default values used in GraphPaper #define GP_AxesColor @"AxesColor" #define GP_LabelColor @"LabelColor" #define GP_GraphColor @"GraphColor" #define GP_xstep @"xstep" #define GP_xmin @"xmin" #define GP_xmax @"xmax" #define GP_ymin @"ymin" #define GP_ymax @"ymax" #define GP_Formula @"Formula"
Add defaults.h
to your GraphPaper project in the
group Other Sources.
We will need to add two methods to each of the GraphView and ColorGraphView classes: an initialize method that will register the appropriate defaults, and an awakeFromNib method that will set the appropriate controls based on the values in the defaults system.
Insert the following declarations into
GraphView.h
:
+ (void)initialize; - (void)awakeFromNib;
Insert the #import
directive and the
#define
macro shown here in bold into
GraphView.m
:
#import "GraphView.h" #import "Segment.h" #import "Label.h" #import "Controller.h"#import "defaults.h" #define FLOAT(x) [NSNumber numberWithFloat:x] @implementation GraphView
Insert the implementation for the initialize class method into
GraphView.m
:
+(void)initialize { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSMutableDictionary *appDefs = [NSMutableDictionary dictionary]; [appDefs setObject:@"0.1" forKey:GP_xstep]; [appDefs setObject:@"-10.0" forKey:GP_xmin]; [appDefs setObject:@"10.0" forKey:GP_xmax]; [appDefs setObject:@"-5.0" forKey:GP_ymin]; [appDefs setObject:@"5.0" forKey:GP_ymax]; [appDefs setObject:@"cos(x)" forKey:GP_Formula]; [defaults registerDefaults:appDefs]; }
For ease of typing and reading, this initialize method uses string values, rather than creating an NSNumber with the appropriate float value.
Next we’ll make the necessary changes to the ColorGraphView class.
Insert the method and function declarations shown here in bold into
ColorGraphView.h
:
+ (void) initialize; - (void) awakeFromNib; @end NSData *DataForColor(NSColor *aColor); NSColor *ColorForData(NSData *data);
Insert the #include
directive shown here in bold
into ColorGraphView.m
:
#import "ColorGraphView.h"
#import "Segment.h"
#import "Label.h"#import "defaults.h"
Insert the following two transformation functions and the class
method declaration for initialize
into ColorGraphView.m
:
NSData *DataForColor(NSColor *aColor) { return [NSArchiver archivedDataWithRootObject:aColor]; } NSColor *ColorForData(NSData *data) { return [NSUnarchiver unarchiveObjectWithData:data]; } @implementation ColorGraphView +(void)initialize { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSMutableDictionary *appDefs = [NSMutableDictionary dictionary]; [appDefs setObject:DataForColor([NSColor redColor]) forKey:GP_AxesColor]; [appDefs setObject:DataForColor([NSColor blueColor]) forKey:GP_LabelColor]; [appDefs setObject:DataForColor([NSColor blackColor]) forKey:GP_GraphColor]; [defaults registerDefaults:appDefs]; }
In addition to providing the initialize method, we must equip the ColorGraphView implementation with two functions — one for converting an NSColor object into an NSData object, and one for converting back. We need to do this because the defaults system cannot store NSColor objects by themselves. The defaults system can store NSData objects, however, so we can store colors in the defaults system by first converting them to NSData objects. (Indeed, because any object can be archived in an NSData structure, it is possible to store any object in the defaults system.)
Notice that the initialize method does not need to call the initialize method in the superclass; the Objective-C runtime system handles this for us automatically.
When GraphPaper starts up, it will read the defaults database to discover the user’s preferences for graph, axes, and label colors. (We’ll add the initial graph parameters to this list in a later section.) To read the database, we use the standardUserDefaults method.
Insert the following awakeFromNib
instance method into GraphView.m
:
- (void)awakeFromNib { NSUserDefaults *defs = [NSUserDefaults standardUserDefaults]; [xminCell setObjectValue:[defs objectForKey:GP_xmin]]; [xmaxCell setObjectValue:[defs objectForKey:GP_xmax]]; [xstepCell setObjectValue:[defs objectForKey:GP_xstep]]; [yminCell setObjectValue:[defs objectForKey:GP_ymin]]; [ymaxCell setObjectValue:[defs objectForKey:GP_ymax]]; [formulaField setObjectValue:[defs objectForKey:GP_Formula]]; }
This method queries the defaults system for the value that corresponds to each key. The first time that this version of GraphPaper is run, these values will correspond to the values that are registered in the initialize class method. However, if any of the values are changed and saved in the defaults system, those values will override the values that are registered.
Insert the following awakeFromNib
method into the ColorGraphView.m
implementation
file:
- (void)awakeFromNib { NSUserDefaults *defs = [NSUserDefaults standardUserDefaults]; [super awakeFromNib]; [self setObjectsToColor:ColorForData([defs dataForKey:GP_AxesColor]) forTag:AXES_TAG]; [self setObjectsToColor:ColorForData([defs dataForKey:GP_GraphColor]) forTag:GRAPH_TAG]; [self setObjectsToColor:ColorForData([defs dataForKey:GP_LabelColor]) forTag:LABEL_TAG]; }
This method queries the defaults system for the NSColor object for each color, then uses the ColorGraphView setObjectsToColor:forTag: method to set the color. We use the setObjectsToColor:forTag: method because it performs the proper sequence of release and retain steps to ensure that we do not leak memory.
Finally, we’ll take advantage of some machinery that is built into the NSWindow class that automatically remembers where the GraphPaper window is dragged by the user and restores the window to that location when the program runs again.
Now let’s test the work we’ve done so far:
Build and run GraphPaper, saving all files first. Click the Graph button. You should see the window shown in Figure 21-3.
Notice that the values for xmin
,
xmax
, step
,
ymin
, ymax
, and
formula
are the values that were registered in the
initialize method, rather than the
values that are stored in the nib.
Move the GraphPaper window to a different location and then quit GraphPaper.
Restart GraphPaper. Notice that the GraphPaper window now appears where you previously left it, rather than in the original location (where the window was positioned in IB).
Now move the GraphPaper window so that it’s partially off-screen.
Quit GraphPaper and restart it again. Note that the window is completely visible!
Quit GraphPaper.
Restoring a window to the location where it was positioned the last time that the application ran is a very friendly feature, but it needs to be implemented correctly. For example, suppose your screen’s resolution is set to 1600 x 1280, and you leave an application’s window in a corner of the screen and then quit the application. If you then lower your screen’s resolution to 1024 x 768 and run the application again, you might not be able to find the application’s window because it is off-screen. Fortunately, the Cocoa implementation of setFrameUsingName: (which we used in Controller’s awakeFromNib method) and related methods will never restore a window in a position where it cannot be seen. These methods interrogate the screen to find out its current resolution and, if the stored frame of the window will not completely appear on the screen, the frame is modified so that it will fit before the window is restored.
Finally, let’s see what the property list for the GraphPaper application looks like at this point:
Restart GraphPaper again, choose GraphPaper → Preferences, and then open the Colors panel and make changes to the colors. Quit GraphPaper.
Now double-click the
~/Library/Preferences/GraphPaper.plist
file in
the Finder to view the GraphPaper defaults in PropertyListEditor, as
shown in Figure 21-4.
The defaults in your GraphPaper.plist
file will
probably differ from those in Figure 21-4. You may
even have different properties listed — it depends on what you
did while GraphPaper was running.
As you can see, other parts of the application kit have been using the defaults system without our knowledge! In particular, the NSColorPanel uses the defaults system to remember its position on the screen as well as its current mode. Note that the main GraphPaper window’s position is stored as well.
Notice also that there is no entry in the defaults system for
xmin
, xmax
,
xstep
, ymin
,
ymax
, or formula
.
That’s because the default values for these items
are never changed in our current code. To do that,
we’ll need to implement the okay: method associated with the GraphPaper
Preferences panel.
[40] Remember, the message sent does not have a plus sign. Because the receiver of the message is a class, the Objective-C runtime looks for a class method called initialize rather than an instance method.
3.144.91.24