TweetFollow Us on Twitter

The Road to Code: Chips or Fries?

Volume Number: 25
Issue Number: 06
Column Tag: The Road to Code

The Road to Code: Chips or Fries?

Handling User Preferences

by Dave Dribin

Introduction

Last month, we covered how to display windows and sheets using canned alerts via the NSAlert class as well as custom windows and sheets stored in separate nibs and displayed with NSWindowController subclasses. This month, we're going to cover how to handle user preferences, as well as how to implement a preferences window that works like most Apple-supplied applications.

I'm going to show you the end result, and then, we'll start filling in the code. The application contains a window with a simple custom NSView subclass that displays your favorite word, as shown in Figure 1:


Figure 1: Favorite Word window

We previously covered custom views, so there's not a lot new, so far. However, I'd like to add in a preferences window so that the user can change their favorite word, as shown in Figure 2:


Figure 2: General preferences

But, let's not stop there. We should let the user customize the background color and text alignment, too, as shown in Figure 3:


Figure 3: Advanced preferences

As you can see, the preferences window is separated into two panes: the General pane and the Advanced pane. It is fairly typical to setup multiple panes in preferences windows to separate options into groups. However, even though this is the standard practice for preferences windows, Apple does not provide us with a ready-made preferences window. We'll have to write a fair amount of code to emulate these standard windows. But don't worry; let me guide you down the road to code.

Main Window

Let's first go over the code to setup the main window. The bulk of the code is in the custom view that displays our favorite word. Create a fresh Cocoa Application project to work on (don't forget to enable garbage collection). Let's dive right in and create a new NSView subclass called WordView. Make the header for WordView match Listing 1:

Listing 1: WordView.h

#import <Cocoa/Cocoa.h>
typedef enum
{
    WordViewLeftTextAlignment,
    WordViewCenterTextAlignment,
    WordViewRightTextAlignment,
} WordViewTextAlignment;
@interface WordView : NSView
{
    NSString * _word;
    NSColor * _backgroundColor;
    WordViewTextAlignment _textAlignment;
}
@property (copy) NSString * word;
@property (copy) NSColor * backgroundColor;
@property WordViewTextAlignment textAlignment;
@end

This is fairly self-explanatory. We've got three instance variables and three properties for the word, background color, and text alignment. The meat is in the implementation, which is shown in full in Listing 2:

Listing 2: WordView.m

#import "WordView.h"
@interface WordView ()
- (void)drawBackground;
- (void)drawWord;
@end
static NSString * RedrawContext = @"RedrawContext";
@implementation WordView
@synthesize word = _word;
@synthesize backgroundColor = _backgroundColor;
@synthesize textAlignment = _textAlignment;
- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self == nil)
        return nil;
    
    _word = @"Word";
    _backgroundColor = [NSColor whiteColor];
    _textAlignment = WordViewCenterTextAlignment;
    
    [self addObserver:self forKeyPath:@"word"
              options:0 context:&RedrawContext];
    [self addObserver:self forKeyPath:@"backgroundColor"
              options:0 context:&RedrawContext];
    [self addObserver:self forKeyPath:@"textAlignment"
              options:0 context:&RedrawContext];
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == &RedrawContext)
        [self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)rect
{
    [self drawBackground];
    [self drawWord];
}
- (void)drawBackground
{
    NSRect bounds = [self bounds];
    
    NSRect pathRect = NSInsetRect(bounds, 2.0, 2.0);
    NSBezierPath * path =
        [NSBezierPath bezierPathWithRoundedRect:pathRect
                                        xRadius:20.0
                                        yRadius:20.0];
    [_backgroundColor set];
    [path fill];
    
    [path setLineWidth:4.0];
    [[NSColor blackColor] set];
    [path stroke];
}
- (void)drawWord
{
    NSRect bounds = [self bounds];
    bounds = NSInsetRect(bounds, 4.0, 4.0);
    
    NSFont * font = [NSFont systemFontOfSize:50];
    NSDictionary * attributes =
        [NSDictionary dictionaryWithObject:font
                                    forKey:NSFontAttributeName];
    NSAttributedString * string = 
        [[NSAttributedString alloc] initWithString:_word
                                        attributes:attributes];
    
    NSSize stringSize = [string size];
    NSPoint point;
    // Center vertically
    point.y = bounds.size.height/2 - stringSize.height/2;
    
    // Align horizonally
    if (_textAlignment == WordViewCenterTextAlignment)
        point.x = bounds.size.width/2 - stringSize.width/2;
    else if (_textAlignment == WordViewLeftTextAlignment)
        point.x = bounds.origin.x;
    else if (_textAlignment == WordViewRightTextAlignment)
        point.x = bounds.size.width - stringSize.width;
    
    [string drawAtPoint:point];
}
@end

Inside the initializer, initWithFrame:, we setup initial values for the word, background color, and text alignment. We also setup key-value observers that monitor these three properties. If any of them change, we need to redraw the view, which is done by calling setNeedsDisplay:. The drawing itself happens inside drawRect: and is delegated to two methods drawBackground and drawWord.

The drawBackground method uses a Bezier path to create a rectangle with rounded corners. First, we fill the path with the background color, and then we stroke it with black to draw the border.

The drawWord method uses a class called NSAttributedString to draw the word with a given font and size. An NSAttributedString is similar to NSString except you can store attributes along with the string. There are many possible attributes, but we are only using the font attribute. Once we have the attributed string, we calculate the correct position inside the view and draw it with the drawAtPoint: method. Remember the origin, point (0, 0), is in the lower-left corner of the view.

Now build the project, fix up any syntax errors, and open up the MainMenu.xib file in Interface Builder. Set the title of the window to Favorite Word. Next, drag a custom view from the library into the window, and set the class of the view to WordView. If you ran the application right now, it would look like Figure 4:


Figure 4: Initial word view

That's all we need for our custom view, so it's time to start getting to the meat of the matter: user preferences.

User Preferences

Cocoa has very good support for user preferences. The main class that provides the interface to user preferences is called NSUserDefaults. Every preference has a name and a value. The name must be a string, and the value must be one of the following classes:

  • NSString

  • NSNumber (for integers, floating point numbers and booleans)
  • NSDate
  • NSData
  • NSArray or NSDictionary of the above classes

So let's cover how to store the favorite word in NSUserDefaults. For the preference name, let's use FavoriteWord (it's fairly customary to capitalize the name), and the value will be whatever the user supplies. Working with NSUserDefaults is very similar to working with a mutable dictionary, and here's how we'd set the user's favorite word to "Pie". In a real application, you wouldn't hard code the value; this is for illustration purposes only:

    NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:@"Pie" forKey:@"FavoriteWord"];

That's it! The system takes care of saving this to a file periodically, so there's nothing else we need to do. Note that you use the +standardUserDefaults class method to get the NSUserDefaults instance, instead of creating a new instance of the class. This method always returns the same object and represents the defaults for your application.

Speaking of preferences files, where does the system store this file? Preferences for all applications automatically go into the directory ~/Library/Preferences. Each application has its own preference file named using its application identifier. Recall that this identifier follows the reverse DNS convention and is set in the info panel of your application. Thus, here is the full name of the preferences file for this application:

~/Library/Preferences/org.dribin.dave.mactech.jun09.Favorite_Word.plist

The extension on this file, plist, stands for property list. Property lists are standard file types for holding configuration information on Mac OS X. There's even a separate application for viewing and editing property lists called Property List Editor. You can use this application to verify that preferences are indeed being saved correctly, for example. Just be aware that the preferences file only exists only after a user changes a preference. It won't exist if the user only uses the standard values.

How do we read preferences using NSUserDefaults? That's just as easy:

NSString * favoriteWord = [defaults objectForKey:@"FavoriteWord"];

Let's put this newfound knowledge into practice. We're going to store the user's favorite word as a string using the FavoriteWord key, as we showed above. For the text alignment, we'll store the integer value of the WordViewTextAlignment enum, which is short for enumrated type. Unlike a mutable dictionary, NSUserDefaults has some convenience methods for storing primitive numbers so you don't have to wrap them up in an NSNumber yourself. Here's an example of how we save the text alignment to the preference named TextAlignment:

    NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
    WordViewTextAlignment alignment = WordViewCenterTextAlignment;
    [defaults setInteger:alignment forKey:@"TextAlignment"];

We can read the value using the integerValueForKey: method.

Storing a color is a bit tricky. You'll notice that NSColor is not one of the supported value classes. Fortunately the NSData type can often be used as a catchall to handle non-standard values such as colors.

Remember that archiving allows you to turn any class that implements the NSCoding protocol into a stream of bytes stored in NSData. The NSColor class implements NSCoding so we just need to convert the color into an NSData before we store it in the user preferences, and then convert the NSData back into a color when reading out of the preferences. Here's how we'd store a red color with the BackgroundColor name using an NSKeyedArchiver to convert an NSColor into NSData:

    NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
    NSColor * color = [NSColor redColor];
    NSData * colorData =
        [NSKeyedArchiver archivedDataWithRootObject:color];
    [defaults setObject:colorData forKey:@"BackgroundColor"];

That's definitely a bit more cumbersome than storing a string, as above, but it's not too bad. Conversely, turning the data back into a color requires using NSKeyedUnarchiver:

    NSData * colorData = [defaults objectForKey:@"BackgroundColor"];
    NSColor * color =
        [NSKeyedUnarchiver unarchiveObjectWithData:colorData];

Let's integrate this into our application. Create a new NSWindowController subclass called MainWindowController. Add an outlet to a WordView instance, as shown in Listing 3.

Listing 3: MainWindowController.h

#import <Cocoa/Cocoa.h>
@class WordView;
@interface MainWindowController : NSWindowController
{
    WordView * _wordView;
}
@property (nonatomic, retain) IBOutlet WordView * wordView;
@end

The corresponding implementation file is shown in Listing 4:

Listing 4: MainWindowController.m

#import "MainWindowController.h"
#import "WordView.h"
NSString * FavoriteWordKey = @"FavoriteWord";
NSString * BackgroundColorKey = @"BackgroundColor";
NSString * TextAligmentKey = @"TextAlignment";
@interface MainWindowController ()
- (void)updateFromDefaults:(NSNotification *)notification;
@end
@implementation MainWindowController
@synthesize wordView = _wordView;
- (void)awakeFromNib
{
    [self updateFromDefaults:nil];
    
    NSNotificationCenter * defaultCenter =
        [NSNotificationCenter defaultCenter];
    [defaultCenter addObserver:self
                      selector:@selector(updateFromDefaults:)
                          name:NSUserDefaultsDidChangeNotification
                        object:nil];
}
- (void)updateFromDefaults:(NSNotification *)notification
{
    NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
    _wordView.word = [defaults objectForKey:FavoriteWordKey];
    
    NSData * colorData = [defaults objectForKey:BackgroundColorKey];
    NSColor * color =
        [NSKeyedUnarchiver unarchiveObjectWithData:colorData];
    _wordView.backgroundColor = color;
    
    WordViewTextAlignment alignment =
        [defaults integerForKey:TextAligmentKey];
    _wordView.textAlignment = alignment;
}
@end

The awakeFromNib method first updates our word view with the values stored in the preferences. But it also subscribes to NSUserDefaultsDidChangeNotification. This allows us to keep up-to-date if the preferences change after the application launches and will be important once we implement the preferences window.

The updateFromDefaults: method uses string constants instead of string literals. This helps reduce simple typo errors when using the same string over and over. The compiler will not let you use a mistyped constant, whereas a mistyped string literal can cause hard to find bugs.

If we ran the application right now, we'd run into a bit of a problem. The first time the user runs the application, their preferences are empty, and so we're not going to get any useful values out of them. What we'd like to do is setup some sensible defaults that the user can later override. We can do this by adding one more method to our implementation:

+ (void)initialize
{
    NSMutableDictionary * defaultValues =
        [NSMutableDictionary dictionary];
    [defaultValues setObject:@"Cocoa" forKey:FavoriteWordKey];
    
    NSColor * color = [NSColor redColor];
    NSData * colorData =
        [NSKeyedArchiver archivedDataWithRootObject:color];
    [defaultValues setObject:colorData forKey:BackgroundColorKey];
    
    NSNumber * alignmentNumber =
        [NSNumber numberWithInt:WordViewCenterTextAlignment];
    [defaultValues setObject:alignmentNumber forKey:TextAligmentKey];
    
    NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
    [defaults registerDefaults:defaultValues];
}

The +initialize method is a class method, not an instance method. It is also special in that it gets called automatically before the class is ever instantiated, even before awakeFromNib. We're using this as an opportunity to register sensible defaults with NSUserDefaults before awakeFromNib ever gets called.

Note that the registerDefaults: method takes a dictionary. Thus, we have to convert the alignment enum into an NSNumber, first. Other than that, we've setup the default favorite word to be "Cocoa", the background to be red, and have centered alignment. If we ran the application right now (don't forget to hookup the wordView outlet), it will look just like Figure 1 above.

Preferences Window

Now that we've got our view all setup and tracking user defaults, we need to have a way for the user to actually edit them. On the one hand, this is fairly easy with Cocoa bindings. On the other hand, creating a preferences window that works like a standard preference window is not trivial.

Start off by creating a new Window XIB file from Xcode and call it Preferences.xib. Then, create a new corresponding window controller named PreferencesWindowController. Override the initializer to use the preferences window nib:

- (id)init
{
    self = [super initWithWindowNibName:@"Preferences"];
    return self;
}
Back in our main window controller add this action method:
- (IBAction)showPreferencesWindow:(id)sender
{
    if (_preferencesWindowController == nil)
    {
        _preferencesWindowController = 
            [[PreferencesWindowController alloc] init];
    }
    [_preferencesWindowController showWindow:self];
}

You'll also need to add the corresponding instance variable to the header. This action method uses the preferences window controller to load and display a preferences window. The menu that you want to connect this to is named Preferences... under the application's menu, as shown in Figure 1.


Figure 5: Preferences menu

We are going to want to customize many of the window attributes of the preferences window. Make sure they all match those in Figure 6.


Figure 6: Preferences window attributes

We now have enough in place that you can test the preferences window. It currently doesn't do anything useful, but you can make sure the Preferences... menu is hooked up properly and displays the preferences window from the nib file.

The first step in creating a standard preferences window is to add a toolbar to this window. Toolbars are typically used to add shortcuts to commonly used actions, but they are also what give preferences windows their distinctive look.

Drag a toolbar out from the Library and onto your preferences window. It comes preconfigured with some standard toolbar items, and while these may be useful for a traditional toolbar, we don't want any of them for our preferences window. Double click on the toolbar and a customize sheet appears, as in Figure 7.


Figure 7: Default toolbar

Drag each and every toolbar item off the Allowed Toolbar Items section to get an empty toolbar. Replace them with two Image Toolbar Items from the Library. Configure the first one on the left to have the attributes in Figure 8. Set the Image Name to NSPreferencesGeneral, both the Label and Pal. Label to General, and the Tag to 0.


Figure 8: General toolbar item

Configure the second toolbar item similarly, setting the Image Name to NSAdvanced, the Label and Pal. Label to Advanced and the Tag to 1, as show in Figure 9.


Figure 9: Advanced toolbar item

Drag each toolbar item from the Allowed Toolbar Items section onto the actual toolbar, and your window should look like Figure 10.


Figure 10: Preferences toolbar

We're done editing the toolbar for now (we'll have to come back and connect actions to the items later), so click on the Done button. Edit the attributes of the toolbar itself to match Figure 11, which should just be unchecking the Customizable checkbox.


Figure 11: Toolbar attributes

One last thing before jumping back to Xcode, set the class of File's Owner to be the PreferencesWindowController and set the delegate of the toolbar to be File's Owner. Also, I want to point out that editing toolbars and toolbar items is new to Interface Builder in Mac OS X 10.5 In previous versions of Mac OS X, you had to create the toolbar and toolbar items all in code.

Back in Xcode, add this toolbar delegate method:

- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
{
    NSMutableArray * identifiers = [NSMutableArray array];
    for (NSToolbarItem * item in [toolbar items])
    {
        [identifiers addObject:[item itemIdentifier]];
    }
    return identifiers;
}

Normally, toolbar items work like push buttons: they are only highlighted when the mouse is down. Selectable toolbar items stay highlighted after the mouse is clicked and are drawn with a special highlight. Our method tells the toolbar that all items are selectable.

View Controllers

Before we finish off the rest of the code for the preferences window controller, let's talk about what we're going to accomplish. Open the preferences for a standard Apple application, such as Mail, iCal, or Address Book. You'll notice that when you click on a toolbar item, the contents of the window are briefly blanked until the window resizes and the contents of the window are replaced with new controls. If you watch closely, you'll notice that the window only resizes vertically. The width stays the same, no matter which preference pane is selected. What's happening is a technique called view swapping.

We're going to put our General preference pane and Advanced preference pane into their own views. Then, when the toolbar is clicked, we're going to swap out the current view and swap in the appropriate view. As another bonus, we're going to store these views in their own nib. Just like keeping windows in their own nib, storing views in their own nib reduces memory consumption by only loading the views as they are needed. If the user never clicks on the Advanced preference pane, it is never loaded into memory.

Just as we use a window controller to load a window from a nib file, there is a class new to Mac OS 10.5 called a view controller that loads a view from a nib file. Let's create our view and view controller for our General preferences pane.

In Xcode, create a new class, name it GeneralPreferencesController, and change the super class to NSViewController, as shown in Listing 5.

Listing 5: GeneralPreferencesController.h

#import <Cocoa/Cocoa.h>
@interface GeneralPreferencesController : NSViewController
{
}
@end

The implementation class is short, as shown in Listing 6.

Listing 6: GeneralPreferencesController.m

#import "GeneralPreferencesController.h"
@implementation GeneralPreferencesController
- (id)init
{
    self = [super initWithNibName:@"GeneralPreferences" bundle:nil];
    return self;
}
@end

All it does is load the correct nib file. You could argue that a separate subclass is not worthwhile in this case, and that's probably true. But real preference panes will most likely need extra code behind them for actions and outlets, so you'd need to create a subclass at that point. We're lucky enough to be able to use Cocoa bindings, but I think it's a good idea to create the subclass up front so you have a place to put code when you need it.

Now create the corresponding nib file by creating a new View XIB file, as shown in Figure 12. Name this nib file GeneralPreferences.xib, and open it up in Interface Builder.


Figure 12: New View XIB file

The first thing you'll want to do is set the File's Owner class to GeneralPreferencesController. Next, you'll want to add a label and a text field to the view. Note that in Interface Builder, our view looks an awful lot like a window. But keep in mind that, despite its looks, it's a bare view without an enclosing window. The final layout should look like Figure 13.


Figure 13: General preferences view

Using Cocoa bindings, we can keep this text field in sync with NSUserDefaults without writing any code. Open up the Bindings section of the Inspector panel for the text field, and bind to Shared User Defaults Controller, setting the Controller Key to values and the Model Key Path to FavoriteWord (with no space) as shown in Figure 14.


Figure 14: Word view bindings

The Shared User Defaults Controller is a special, built-in object controller that connects directly to the shared NSUserDefaults. The model key path is the name of the preference you want to bind to, so this must be the same string we used in the main window controller. And through the magic of bindings, we've successfully allowed the user to edit their favorite word.

We now have to go through similar steps for the Advanced preference pane. Create a new view controller subclass, but this time name it AdvancedPreferencesController. Override the initializer and load the nib file named AdvancedPreferences. Finally, create a new View XIB file named AdvancedPreferences.xib and open this in Interface Builder.

Again, the first step is to change the File's Owner class to be AdvancedPreferencesController. Layout the view to match Figure 15 by dragging two labels, a color well, and a radio button group from the Library onto the view. By default, a radio group only has two buttons. To create the third button, drag down as if you were resizing the view, but hold down the Option key.


Figure 15: Advanced preferences view

Again, we can connect the color well and radio button group using Cocoa bindings. For the color well, bind the Value to the Shared User Defaults Controller, but this time use BackgroundColor as the Model Key Path. We also have to deal with the fact that the color is stored in the preferences as NSData. Change the Value Transformer to be NSKeyedUnarchiveFromData. Value transformers act as a middleman between the view and the controller. There are various built-in transformers, and you can create your own, but we can use the one that archives and unarchives the value. The bindings options should match Figure 16.


Figure 16: Color well bindings

For the radio button group, you are going to bind the Selection Indexes to Shared User Defaults controller. Set the Model Key Path to TextAlignment. There's no need to change anything else, as it will automatically convert to and from an NSNumber instance.

Make sure both view nibs are saved, and it's time to head back into Xcode to code up the view swapping. Update the header file for PreferencesWindowController to match Listing 7. We've added two instance variables, one for each view controller, and an action method that the toolbar items will use.

Listing 7: PreferencesWindowController.h

#import <Cocoa/Cocoa.h>
@class GeneralPreferencesController;
@class AdvancedPreferencesController;
@interface PreferencesWindowController : NSWindowController
{
    GeneralPreferencesController * _generalPreferences;
    AdvancedPreferencesController * _advancedPreferences;
}
- (IBAction)changePreferencePane:(id)sender;
@end
In the implementation file, add the accessors for the view controllers:
- (GeneralPreferencesController *)generalPreferences
{
    if (_generalPreferences == nil)
    {
        _generalPreferences =
            [[GeneralPreferencesController alloc] init];
    }
    return _generalPreferences;
}
- (AdvancedPreferencesController *)advancedPreferences
{
    if (_advancedPreferences == nil)
    {
        _advancedPreferences = 
            [[AdvancedPreferencesController alloc] init];
    }
    return _advancedPreferences;
}

These create the objects as needed. Again, this keeps memory consumption down by only creating objects when they are needed. Next, add these three methods that implement the view swapping:

- (IBAction)changePreferencePane:(id)sender
{
    [self selectPreferencesForItem:sender animate:YES];
}
- (void)selectPreferencesForItem:(NSToolbarItem *)item
                         animate:(BOOL)animate
{
    NSInteger tag = [item tag];
    NSViewController * preferencesController = nil;
    if (tag == PreferencesGeneralTag)
        preferencesController = [self generalPreferences];
    else if (tag == PreferencesAdvancedTag)
        preferencesController = [self advancedPreferences];
    
    [self selectPreferences:preferencesController animate:animate];
    [[self window] setTitle:[item label]];
}
- (void)selectPreferences:(NSViewController *)preferences
                  animate:(BOOL)animate
{
    NSView * contentView = [[self window] contentView];
    NSView * preferencesView = [preferences view];
    // Calculate the change in height
    NSSize currentSize = [contentView frame].size;
    NSSize newSize = [preferencesView frame].size;
    CGFloat deltaHeight = newSize.height - currentSize.height;
    
    // Calculate the window's new frame
    NSWindow * window = [self window];
    NSRect windowFrame = [window frame];
    windowFrame.size.height += deltaHeight;
    windowFrame.origin.y -= deltaHeight;
    // Remove the current view
    for (NSView * view in [contentView subviews])
        [view removeFromSuperview];
    // Resize the window
    [window setFrame:windowFrame display:YES
             animate:animate];
    
    // Resize the new view's width
    newSize.width = currentSize.width;
    [preferencesView setFrameSize:newSize];
    
    // Add it to the window
    [contentView addSubview:preferencesView];
}

Let's work our way through these methods from the top down. The first method is our action method that gets called when either of the toolbar items is clicked. The sender of the action will be the toolbar item that the user clicked. This simply calls into the selectPreferencesForItem: method with the animate argument set to YES.

The second method uses the tag of the toolbar item to select the correct view controller. We use an enum to map the tag values into compile time constants. This ultimately calls through to the third method, selectPreferences:animate:, which does the actual view swapping. After the view swapping is finished, it sets the title of the window to be the same as the toolbar item label.

The algorithm for view swapping is fairly simple: remove the existing view, resize the window with or without animation, and add in the new view. The only tricky part is knowing how much to resize the window. We compute the difference in height between the current view and the view we are swapping to, and change the frame of the window by that same amount. Remember, the origin is in the lower-left, again, so we need to adjust the origin so that the top of the window does not move. The setFrame:display:animate: method does the fancy animation for us. All we need to do is remove the current view before resizing the window and add in the new view when it's finished. We also ensure the new view's width is resized to the width of the window.

That's the bulk of it. We need to make sure to connect up the toolbar items to the changePreferencePane: action and add in two more methods for some final touches:

- (void)showWindow:(id)sender
{
    NSWindow * window = [self window];
    if (![window isVisible])
        [window center];
    
    [super showWindow:sender];
}
- (void)windowDidLoad
{
    NSToolbar * toolbar = [[self window] toolbar];
    NSToolbarItem * firstItem = [[toolbar items] objectAtIndex:0];
    [toolbar setSelectedItemIdentifier:[firstItem itemIdentifier]];
    [self selectPreferencesForItem:firstItem animate:NO];
}

The first method overrides the default implementation of showWindow: to center the window on screen before displaying it. This is not strictly necessary, but I find it looks nicer. The windowDidLoad method is necessary to ensure that the General preferences view is initially swapped in. Notice that we're using the same method the toolbar action method uses, but we're setting animate to NO, as we want the window to display immediately without any animation.

Conclusion

This has probably been the longest example we've done so far. If you don't want to type in all this code, feel free to download the completed project from the MacTech website. Congrats for keeping up. More goodies to come next month in The Road to Code.


Dave Dribin has been writing professional software for over eleven years. After five years programming embedded C in the telecom industry and a brief stint riding the Internet bubble, he decided to venture out on his own. Since 2001, he has been providing independent consulting services, and in 2006, he founded Bit Maki, Inc. Find out more at http://www.bitmaki.com/ and http://www.dribin.org/dave/.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Things 2.5.4 - Elegant personal task man...
Things is a task management solution that helps to organize your tasks in an elegant and intuitive way. Things combines powerful features with simplicity through the use of tags and its intelligent... Read more
NeoOffice 2014.10 - Mac-tailored, OpenOf...
NeoOffice is a complete office suite for OS X. With NeoOffice, users can view, edit, and save OpenOffice documents, PDF files, and most Microsoft Word, Excel, and PowerPoint documents. NeoOffice 3.x... Read more
iPhoto Library Manager 4.2 - Manage mult...
iPhoto Library Manager allows you to organize your photos among multiple iPhoto libraries, rather than having to store all of your photos in one giant library. You can browse the photos in all your... Read more
Web Snapper 3.3.8 - Capture entire Web p...
Web Snapper lets you capture Web pages exactly as they appear in your browser. You can send them to a file as images or vector-based, multi-page PDFs. It captures the whole Web page - eliminating the... Read more
TeamViewer 10.0.41404 - Establish remote...
TeamViewer gives you remote control of any computer or Mac over the Internet within seconds, or can be used for online meetings. Find out why more than 200 million users trust TeamViewer! Free for... Read more
Ableton Live 9.1.8 - Record music using...
Ableton Live lets you create and record music on your Mac. Use digital instruments, pre-recorded sounds, and sampled loops to arrange, produce, and perform your music like never before. Ableton Live... Read more
VOX 2.5 - Music player that supports man...
VOX is a beautiful music player that supports many filetypes. The beauty is in its simplicity, yet behind the minimal exterior lies a powerful music player with a ton of features & support for... Read more
OmniFocus 2.1.2 - GTD task manager with...
OmniFocus helps you manage your tasks the way that you want, freeing you to focus your attention on the things that matter to you most. Capturing tasks and ideas is always a keyboard shortcut away in... Read more
Adobe Flash Player 17.0.0.169 - Plug-in...
Adobe Flash Player is a cross-platform, browser-based application runtime that provides uncompromised viewing of expressive applications, content, and videos across browsers and operating systems.... Read more
iMazing 1.1.3 - Complete iOS device mana...
iMazing (was DiskAid) is the ultimate iOS device manager with capabilities far beyond what iTunes offers. With iMazing and your iOS device (iPhone, iPad, or iPod), you can: Copy music to and from... Read more

Chainsaw Warrior: Lords of the Night has...
It's time to put the Darkness back in its place now that Chainsaw Warrior: Lords of the Night has officially made it to iOS. | Read more »
A World of Ice and Fire Lets You Stalk 2...
George R. R. Martin’s A World of Ice and Fire, by Random House, is a mobile guide to the epic series. The new update gives you the Journeys map feture that will let you track the movements of 25 different characters. But don't worry, you can protect... | Read more »
Gameloft Announces Battle Odyssey, a New...
Battle Odyssey, Gameloft's newest puzzle RPG, is coming to the App Store next week. Set in the world of Pondera, you will need to control the power of the elements to defend the world from evil. You'll be able to entlist over 500 allies to aid you... | Read more »
Fusion - HDR Camera (Photography)
Fusion - HDR Camera 1.0.0 Device: iOS Universal Category: Photography Price: $1.99, Version: 1.0.0 (iTunes) Description: Fusion creates HDR (high dynamic range) photos by capturing different exposures and then combining them into one... | Read more »
Sago Mini Toolbox (Education)
Sago Mini Toolbox 1.1 Device: iOS Universal Category: Education Price: $2.99, Version: 1.1 (iTunes) Description: Come build with the Sago Mini friends! Use a wrench, try a saw, or hammer some nails. From sewing hand puppets to... | Read more »
You Should Probably Grab Hitman GO While...
Hitman GO is a surprisingly cool (yet also incredibly drastic) departure from the Hitman series. It's well worth playing for any puzzle game fans out there, and at the moment you can get your hands - or garrotte if you will - on it for a mere $0.99... | Read more »
IFTTT is Bringing Do Button and Do Note...
IFTTT has announced Do Button and Do Note for the Apple Watch. Do Button lets you make your own personalized button that can connect to things like your Google Drive, control the temperature in your home with Nest Thermostat, or turn the lights on... | Read more »
How Many Days, Hours, and Minutes Are Le...
Countdown, by Yves Tscherry, is now available on the App Store. The app keeps track of countdowns to your favorite things such as someones birthday or days till the New Year. You can display the time in seconds, minutes, hours, days, weeks, months,... | Read more »
The All-New Misfit 2.0 App is Available...
Misfit has just given their app a complete overhaul. Misfit 2.0 now has a brand new interface with a sleek design and is easier to navigate. You'll be able to sync your Misfit device and look up health and fitness information faster than ever before... | Read more »
Halo: Spartan Strike (Games)
Halo: Spartan Strike 1.0 Device: iOS Universal Category: Games Price: $5.99, Version: 1.0 (iTunes) Description: Delve into 30 challenging missions through cities and jungles using a devastating arsenal of weapons, abilities and... | Read more »

Price Scanner via MacPrices.net

Clearance 13-inch 2.6GHz Retina MacBook Pro a...
B&H Photo has clearance 2014 13″ 2.6GHz/128GB Retina MacBook Pros now available for $1099, or $200 off original MSRP. Shipping is free, and B&H charges NY sales tax only. Read more
Apple refurbished 2014 13-inch Retina MacBook...
The Apple Store has Apple Certified Refurbished 2014 13″ Retina MacBook Pros available for up to $400 off original MSRP, starting at $979. An Apple one-year warranty is included with each model, and... Read more
iMacs on sale for up to $205 off MSRP, NY tax...
B&H Photo has 21″ and 27″ iMacs on sale for up to $205 off MSRP including free shipping plus NY sales tax only: - 21″ 1.4GHz iMac: $1019 $80 off - 21″ 2.7GHz iMac: $1189 $110 off - 21″ 2.9GHz... Read more
Sale! 16GB iPhone 5S for $1 with service
Best Buy is offering 16GB iPhone 5Ss for $1.00 with 2-year activation at a participating cellular provider. Choose free home shipping and activation, or buy online and activate during in-store pickup... Read more
Apple refurbished 2014 MacBook Airs available...
The Apple Store has Apple Certified Refurbished 2014 MacBook Airs available starting at $679. An Apple one-year warranty is included with each MacBook, and shipping is free. These are currently the... Read more
27-inch 3.5GHz 5K iMac on sale for $2349, sav...
 Adorama has the 27″ 3.5GHz 5K iMac in stock today and on sale for $2349 including free shipping plus NY & NJ sales tax only. Their price is $150 off MSRP. For a limited time, Adorama will... Read more
Save up to $380 on an iMac with Apple refurbi...
The Apple Store has Apple Certified Refurbished iMacs available for up to $380 off the cost of new models. Apple’s one-year warranty is standard, and shipping is free: - 27″ 3.5GHz 5K iMac – $2119 $... Read more
iFixIt Teardown Awards 12-IInch Retina MacBoo...
iFixIt has posted its illustrated teardown of the new 12-inch MacBook Retina. They note that this new MacBook is less than half the thickness of the last Apple notebook called just “MacBook” back in... Read more
Faithful iPad 2 Gets A Second Career In Retir...
Finally, after four months’ transition, I handed my faithful old 2011 iPad 2 off to my wife at the end of March and switched whole-hog to using the iPad Air 2 I bought back in November. I’d found... Read more
Apple’s Worldwide Developers Conference Opens...
Apple has announced that it will hold its 26th annual Worldwide Developers Conference (WWDC) from June 8 through June 12 at San Francisco’s Moscone West, with more sessions than ever before streamed... Read more

Jobs Board

*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Solutions Consultant - Retail Sales...
**Job Summary** As an Apple Solutions Consultant (ASC) you are the link between our customers and our products. Your role is to drive the Apple business in a retail Read more
*Apple* TV Live Streaming Frameworks Test En...
**Job Summary** Work and contribute towards the engineering of Apple 's state-of-the-art products involving video, audio, and graphics in Interactive Media Group (IMG) at Read more
Event Director, *Apple* Retail Marketing -...
…This senior level position is responsible for leading and imagining the Apple Retail Team's global engagement strategy and team. Delivering an overarching brand Read more
*Apple* Solutions Consultant - Retail Sales...
**Job Summary** As an Apple Solutions Consultant (ASC) you are the link between our customers and our products. Your role is to drive the Apple business in a retail Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.