oreilly.comSafari Books Online.Conferences.


AddThis Social Bookmark Button

Cocoa Diversions; More on Views
Pages: 1, 2, 3

In Project Builder

Now this is where the fun really starts! In Project Builder let's start out with the awakeFromNib method in Controller. Since we didn’t put an interface in the main window in Interface Builder, we want to set one of the views to be the initial content view of the window at start up. A content view in a window is the view that contains the window’s contents, much the same as the content view of a drawer. To set a content view for a window we use NSWindow’s -setContentView: method (fancy that), with the view we want to set as the content view supplied as the argument. We also have to resize the window to fit the view we’re setting as the content view, otherwise the window will be sized as it was in Interface Builder.

Another thing we want to do is obtain and store in instance variables the sizes of the frame rectangles of the two views we’re working with. To support this add the instance variables in Controller.h, shown below:

NSSize smallSize, largeSize;

Finally, we’re going to create a view that is blank with no controls. This will be used as an intermediate view to display while the window is resizing. Add the instance variable NSView *blankView to Controller.h as well. Controller.h should now look like the following:

@interface Controller : NSObject
    IBOutlet NSView *largeView;
    IBOutlet NSWindow *mainWindow;
    IBOutlet NSView *smallView;

    NSSize smallSize, largeSize;
    NSView *blankView;
- (IBAction)goLarge:(id)sender;
- (IBAction)goSmall:(id)sender;


Now let's take a look at –awakeFromNib in Controller.m :

- (void)awakeFromNib
    smallSize = [smallView frame].size;
    largeSize = [largeView frame].size;

    [mainWindow setContentSize:smallSize];
    [mainWindow setContentView:smallView];
    blankView = [[NSView alloc] init];

In the first two lines we use the NSView method--a frame to return the receiver’s frame rectangle--then using the structure member operator, we obtain the size member of the NSRect and assign it to the appropriate variable. This is done for both the small and large views. In the next two lines we set the window’s content size to smallSize, and we set the contentView of mainWindow to be smallView. Finally, before leaving awakeFromNib we create and initialize an instance of NSView and assign it to blankView. With these preliminaries out of the way lets get into the meat of this class.

We’ll now create a method called – resizeWindowToSize:. The argument of this method is an NSSize datatype-—the size that we want to make the new window. Let's dive right into this method and then we’ll go through it piece by piece.

- (void)resizeWindowToSize:(NSSize)newSize
    NSRect aFrame;
    float newHeight = newSize.height;
    float newWidth = newSize.width;

    aFrame = [NSWindow contentRectForFrameRect:[mainWindow frame] 
styleMask:[mainWindow styleMask]];
    aFrame.origin.y += aFrame.size.height;
    aFrame.origin.y -= newHeight;
    aFrame.size.height = newHeight;
    aFrame.size.width = newWidth;
    aFrame = [NSWindow frameRectForContentRect:aFrame 
styleMask:[mainWindow styleMask]];
    [mainWindow setFrame:aFrame display:YES animate:YES];

The first thing we do after declaring a local NSRect variable to work with is to set the variables newHeight and newWidth to the values of the height and width members of the newSize argument. These values will be used in calculating the new dimensions and location of the window’s frame.

The next line is an important piece of code. What is does, in effect, is return the NSRect for the frame of the window’s current content view. Why don’t we just work with the rect returned by [mainWindow frame]? Because -frame returns the NSRect that defines the entire window-—we want to neglect the title bar and any other "style" elements of the window. We invoke in NSWindow the class method +contentRectForFrameRect:styleMask:. The NSWindow class object takes the frame of mainWindow as the first argument, and then the style mask of mainWindow. The style mask tells us what style elements the window uses, such as a title bar. We don’t have to worry about the particulars of style masks here—we just ask mainWindow what its style mask is using –styleMask and pass that along to +contentRect….In the image below –frame returns the rectangle drawn in green, and +contentRect… takes that rectangle and returns the rectangle drawn in red.

What the offsets mean for a drawer attached to the left or right of the parent window.

In the next four lines we do some clever math on the dimensions of the frame returned by +contentRectForFrameRect:styleMask: to resize it to newWidth and newSize. The purpose of this is to ensure that the upper-left corner of the window doesn’t change when we resize the window.

Also in Programming with Cocoa

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Build an eDoc Reader for Your iPod, Part 3

The next method, +frameRectForContentRect:styleMask: does the exact opposite of what +contentRectForFrameRect:styleMask: did. That is, it takes a content rectangle (red rect in the image above), and calculates the frame for a window (green rect above) by taking into account the style mask for the particular window.

Finally, we make the magic happen in the last line of code with - setFrame:display:animate:. This method takes aFrame—-the frame we want to resize the window to-—as the first argument and two booleans as the last two arguments. The display: argument tells the window whether it should redraw at each step of the animation, and animate: tells the receiver whether the resize operation should be animated.

So why all the hubbub just to set the frame of a window? Couldn’t we just have passed the frame of the desired view to setFrame:…? The reason is that frames, as NSRects, are more than sizes; they specify a location on the screen as well. Thus, blindly passing a the frame of smallView or largeView to this method would do more than resize the window; the window would fly across the screen as well, which is definitely what we don’t want. We want to maintain the position of the top-left corner of the window.

Now, let's get to those two long-forgotten action methods, goSmall: and goLarge:. With the creation of -resizeWindowToSize:, implementing these two methods will not be difficult. The implementations for these two methods are the same, except for using two different views. The following is the code for these two methods:

- (IBAction)goLarge:(id)sender
    [mainWindow setContentView:blankView];
    [self resizeWindowToSize:largeSize];
    [mainWindow setContentView:largeView];

- (IBAction)goSmall:(id)sender
    [mainWindow setContentView:blankView];
    [self resizeWindowToSize:smallSize];
    [mainWindow setContentView:smallView];

Notice that the first thing we do in each of these methods is to set the content view of mainWindow to blankView. The reason we do this is to make the resize transition a little easier on the eyes. If we didn’t do this we would see the old content view stay where it started while the resize happens around it.

System Preferences does something similar by having a transition view show the icon of the preference pane and a message indicating that the pane is loading. After you get things working, try commenting out these lines to see what happens. With blankView set as the contentView of mainWindow, we now invoke our –resize… method with the desired NSSize instance variable passed along as the argument. Finally, we set the content view of the window to the desired view.

So that’s the story. Give it a go and see how it works. With the drawer open you will see the effects of those offset variables-—the drawer resizes along with the window. I hope you’ve enjoyed learning about these two Aqua enhancements. Next time we're going to look into some more new stuff from Aqua as we learn about toolbars and how to add one to your application. The project folder can be downloaded here. I hope you’ve enjoyed learning about these two Aqua enhancements.