Posts Tagged ‘NSView’

NSViewController, the New C in MVC - Pt. 3 of 3

Monday, May 26th, 2008

The last two installments in this series focused on the challenge of integrating NSViewController into the controller layer of the current Cocoa MVC application architecture. This is an important step to take, especially when developing a Cocoa application with a single window interface design. In a single window design, users will access most of the application’s features through one window rather than opening and closing several smaller windows as they are needed. Since this one window will never close during a session, the multiple view controllers in the window will fill the role previously held by multiple window controllers in creating a dynamic Cocoa application.

The application’s view controllers will perform tasks like loading/unloading the content of nib files and their controllers as needed and validating/invalidating menu items as features are added or removed from the window. Plugging NSViewController into the existing controller architecture is a simple way to provide it with the support it needs to fulfill these standard duties for your application.

However important these window controller-type tasks are, keep in mind that they are not the principle duty of a view controller. Unlike NSWindowController, NSViewController has a close relationship to the view layer of the Cocoa MVC design. Each view controller manages a view in a view hierarchy. A system of view controllers, like the system Jonathan Dann and I described in the previous installment, manages the entire view hierarchy of a window. One of the most mission-critical jobs of a system of view controllers is to build and maintain the structure and layout of a complex view hierarchy in a dynamic environment.

lightroom.jpg

Above: Adobe Photoshop Lightroom is an example of a feature rich application with a single window interface design.

Quick Quiz: what is a view hierarchy?

Quick answer from the docs (emphasis is mine):

In addition to being responsible for drawing and handling user events, a view instance can act as a container, enclosing other view instances. Those views are linked together creating a view hierarchy. Unlike a class hierarchy, which defines the lineage of a class, the view hierarchy defines the layout of views relative to other views.

V + C, the dynamic layout duo

There is a key difference between developing a Cocoa application that spreads its interface over several windows and one that uses a single window interface. In a single window design, the view hierarchy of the application’s main window is much more complex and, most importantly, dynamic than Cocoa windows normally are. In fact, the view hierarchy is a prominent character in this type of development. Managing its structure and layout throughout the application’s runtime is a significant design problem that should be addressed in a systematic way by the software’s MVC architecture – in both the controller and view layers.

NSViewController addresses this issue by providing support for building and managing the structure of a view hierarchy through its “view” instance variable. Once you organize your view controllers into a coherent system, you have a convenient mechanism for accessing and adjusting the hierarchy from its significant branches. But the problem is only half solved since changes in the view hierarchy are reflected by changes in the layout, and unfortunately, the issue of layout in a dynamic view environment remains to be addressed by Cocoa.

Building and maintaining a complex layout, like the one pictured above, requires cooperation from both the views and the controllers of a window. The controllers are the dictators of the layout. They tell the views contained in their domain of the hierarchy where to go and how to behave. Unless the circumstances are very special, it’s up to the view hierarchy itself to do the calculations necessary to maintain the integrity of that layout as the forces within it change over time. If either member of the layout duo fails to perform their duty, the interface will simply break.

The problem with V

In Cocoa, NSView fails to perform its duty in this kind of system because it isn’t equipped with the right tools. NSView has something called an “autoresizing mask”, which is meant to define the layout behavior of a view when its superview’s layout changes. However, Autoresizing was never designed to function in a view hierarchy with a dynamic structure. It is simply not the right tool for this job. This is evidenced by one simple thing: Autoresizing requires that a view is already laid out within its superview before you are supposed to set the desired resizing behavior.

This is well and good when you’re creating your entire layout in Interface Builder, where all the views are present and accounted for, but it’s simply not a practical limitation to make in a system of views and view controllers where elements of the layout will be added and removed during the application’s runtime.

It means that a view controller must have knowledge of it’s view’s superview and its frame before it can technically set a layout, but view controllers don’t necessarily have that information. For instance, when a view controller is initialized, it will create a view that will eventually be added to the view hierarchy. At that moment, however, it doesn’t know anything about the view/controller hierarchy that it and its view are about to become a part of. Technically, there’s no way for the controller to guarantee that the layout it dictates to its view (or more specifically, the view’s subviews) won’t be damaged by the layout of the view above it in the hierarchy. Since NSViewController can’t always meet the requirements set forth by Autoresizing, the layout is exposed to the glitches and bugs that I’ve described in previous posts by default, and there’s nothing to be done about it except to take extra special care to explicitly size and position new views when adding them into the hierarchy and to set limits on how much the user is allowed to resize elements like split views and the window itself. This is just extra grunt-work code that isn’t *really* necessary. It’s a byproduct of the bugs in Autoresizing.

The bigger problem, one that does sometimes affect the design of the controllers, is that when a view controller makes a change to the existing view hierarchy, by either adding or removing views or manually adjusting the size or position of a view, it needs to take the view’s siblings into consideration. The quote I pasted earlier, defining a view hierarchy, states that:

the view hierarchy defines the layout of views relative to other views.

What the docs really mean is that the layout of views is defined relative to their superviews. The sibling views aren’t considered by the view hierarchy at all during autoresizing. So, any sibling that needs to move or resize as a result of the change needs to have its layout recalculated and its autoresizing mask re-set by the view controller that propagated the change – even if the affected views aren’t in the controller’s domain of the view hierarchy. In this situation, the dynamic layout behavior of the view hierarchy becomes the responsibility of the view controller to maintain. This slippage of responsibility results in case-by-case workaround code that’s not only a pain in the ass to maintain, but that sometimes requires dependencies to views contained by other view controllers to be hard coded into the view controller. All this does is make your view controllers less reusable by no fault of their own. Again, none of this is necessary, it’s a byproduct of the limitations of Autoresizing.

Absolute vs. Relative

At the heart of the problem with Autoresizing, specifically in a dynamic system of views and view controllers, is the fact that there’s just nothing dynamic about it. The mechanism is based on absolute positions and sizes, which makes it an inappropriate layout tool for this kind of interface development. There are very few absolutes in a single window interface design. Absolutes might include the height of a toolbar or the width of a control, but the state of the view hierarchy as a whole, at any given time during the app’s runtime, is variable.

A dynamic view hierarchy and its controller counterparts would be better served by a layout tool that can accommodate relative sizes and positions. In a relative system, a view controller could just tell a newly created view to, for example, *fill* the width and height of its superview or *float* upwards in its superview – without having to specify any specific size or position (which might not be known anyway) – and the view hierarchy would work out the details once the view is in there.

Remember, view controllers have A LOT of responsibility in a Cocoa applicaiton. They handle action events, load nib files, validate menu items, and sync up views to their data. Why add managing the autoresizing behavior of the view hierarchy to their already long list of chores?

Still more to come…

So, yeah, I warned about the rant ;) Now I’ll try to offer a solution.

I’ve prepared an example project that illustrates how to use Jonathan’s and my controller subclasses, XSWindowController and XSViewController to build and manage a dynamic view hierarchy. The project introduces an NSView subclass, KTView, into the mix. Naturally, KTView uses an alternative to the Autoresizing mechanism that’s specifically designed to work within a changing view hierarchy. It’s not a perfect solution (only Apple can do that), but it does get rid of the extra code from the view controllers that’s only there as a byproduct of Autoresizing.

The goal of the project is to bring together the concepts and opinions that have been covered in these three posts through examples of NSViewController in action.

I’ve decided that I don’t want to release KTView without an Interface Builder plugin, so I’ll publish the project with a tutorial as soon as that’s finished.

Until then, I found the original Smalltalk MVC Design paper, How to use Model-View-Controller. It’s interesting to read how Cocoa’s MVC design deviated from the original idea, especially when it comes to views and controllers. Enjoy!

NSView and Autresizing. Yes, Again.

Thursday, March 20th, 2008

The first post I made on this blog had to do with NSView’s autoresizing behavior being unreliable. As a Cocoa UI developer, this is a real problem that I just have to deal with. But, that’s not the worst thing about autoresizing in NSView. The worst thing about autoresizing in NSView is that setting an autoresizing mask requires that your view controller or window controller explicitly set the size and position of the view within its superview before it can set the autoresizing mask. And whenever the view hierarchy changes, lets say that you need your layout to accommodate a new sibling view at some level of the hierarchy, you have to recalculate everyone’s frame and reset everyone’s mask at that level. Icky, dirty code.

I can see that Cocoa’s getting more sophisticated with how it approaches layout. We can use “layout managers” with CALayers that have this great “constraints” system. From the docs:

Constraint-based layout allows you to describe the position and size of a layer by specifying relationships between a layer and its sibling layers or its superlayer

Relationships. That makes more sense. I want my views to have relationships to each other and I want the views or some layout manager to have the responsibility of maintaining those relationships when the view hierarchy changes, not the controller classes. That’s not what view controllers and window controllers are for.

So I wonder why all this great layout development for layers and not views? Since layers aren’t in the responder chain, they’re not a viable view substitute and I can’t imagine that CALayer will ever become an NSResponder because of its relationship to another framework that we can’t talk about. Maybe NSView is on its way out? There are slight hints that some changes are underway, but even if that’s the case, I can’t imagine that it’ll happen any time soon. NSView does a lot. So, how about an NSViewLayoutManager class in the mean time? Or new options for autoresizing masks? Yay! We Love View!

I filed a bug report requesting a layout manager class for NSView: Bug ID# 5809928.

Welcome, Designers

Monday, March 10th, 2008

My head has been reeling all weekend after watching the iPhone SDK presentation. The excitement I felt the first time I saw an iPhone has returned full-force after it was crushed by Steve Jobs during last year’s WWDC keynote address. I still think he owes all of us an apology for trying to pass off Ajax techniques as an SDK like we’re stupid. Anyway, I’m super happy and optimistic about all the new technology we get to play with and like many others, I can’t help but think about the future. There’s one thing in particular that I can’t get off my mind.

There are going to be lots of new Cocoa developers and many of them will come from the field of design, not software development. Some will have never programmed before in their life. This happened with HTML and CSS when the web became popular. It’s going to happen in the world of iPhone development and Mac desktop application development will naturally follow.

This influx of new developers means that the usability of the Cocoa’s APIs are going to be put to the test like never before. Bugs like NSView’s autoresizing not working as advertised or the fact that the methods to customize the drag and drop highlights of an NSTableView are private and not legally accessible to developers are not going to sit well with this new generation of Mac devs, who will demand control over every aspect of their visual design.

It should be possible for the Cocoa engineers to give developers complete control over the visual properties and interactive behaviors of their classes. I don’t mean in that convoluted creating a custom cell way, it should be easy — HTML and CSS easy (in fact, why don’t we use style sheets?). If there are points where this is technically impossible, it’s a design flaw of the framework and should be fixed. If it means writing a new NSTableView, so be it.

Sorry, you’re screwed

I keep thinking of an article I read about the history of CSS and this part in particular:

Meanwhile, writers of Web pages were complaining that they didn’t have enough influence over how their pages looked. One of the first questions from an author new to the Web was how to change fonts and colors of elements. HTML at that time did not provide this functionality - and rightfully so. This excerpt from a message sent to the www-talk mailing list early in 1994, gives a sense of the tensions between authors and implementors:

In fact, it has been a constant source of delight for me over the past year to get to continually tell hordes (literally) of people who want to — strap yourselves in, here it comes — control what their documents look like in ways that would be trivial in TeX, Microsoft Word, and every other common text processing environment: “Sorry, you’re screwed.”

The author of the message was Marc Andreessen, one of the programmers behind NCSA Mosaic. He later became a co-founder of Netscape and by then his views - if they ever were his views - on formatting had changed.

I doubt that any Cocoa engineer has such a flippant attitude towards designers. After all, this is OS X — the best looking operating system on the market. I just hope that they will take these types of issues into more serious consideration when mapping out their priorities. Sure, advertising “New Table Views!” in Cocoa isn’t as sexy a feature as Core Animation, but it’ll make everyone’s life easier in the long run. Imagine this question popping up on the Cocoa-dev list: “How do I customize the background color of a column in an NSTableView”. Now think of the answer…

Creating a Custom Control with NSView

Friday, March 7th, 2008
AsimoControl.jpg

This tutorial is about implementing a custom control class. The goal is not to explain how to subclass NSControl or NSCell, but how to think more generally about controls, what they are and how to implement one with an NSView subclass. As with the last tutorial, it will be most useful if you download the example project and follow along in the source code. The comments will provide the implementation details. It’s also fun to give Asimo a pompadour.

The tutorial is based on a similar presentation that I gave at a New York City CocoaHeads meeting a few months ago.

What is a Control?

A control is an interface element that allows users to manipulate data in an application. It’s main responsibilities are to draw itself, handle events, and communicate its values to other parts of the application.

Cocoa’s AppKit framework comes with several controls out of the box:

Picture 43.png
Picture 45.png
Picture 46.png

All of the controls pictured above are decedents of Cocoa’s control class, NSControl. As the documentation states, NSControl works closely with NSCell to provide the basic features of a user interface object. In most cases, you can use an AppKit control with no extra code.

Why Make a Custom Control?

With all of AppKit’s ready-made controls, why would you need to implement your own? Here are two situations where the Cocoa control classes are not a good choice for your interface:

1. None of the AppKit controls support the interaction model you want to implement

Take Photoshop’s curves interface as an example. This interface requires that the user is able to add control points to a bezier curve. They must then be able to select the control points and drag them around to adjust the values of the curve. If they click on the pencil button, the interface must switch to a mode that lets them draw the curve directly into the view.

PSCurves.jpg

You’re not going to find an out of the box control that supports this interface in AppKit and NSControl may not be an appropriate place to start your custom implementation. If you look at its API, it is clear that NSControl was designed to create the AppKit controls. They all have fairly straightforward drawing, event handling and target/action needs. Point-and-click. If you were to use NSControl as a starting point for this interface, you would need to create a few custom cell classes that you would manage in your control’s custom cell. At this point, you’re asking for a headache. This interface is not what NSControl was designed for. You’re going to spend more time fighting with the framework than implementing your ideas.

2. You are drawing with OpenGL instead of Quartz

Picture 53(2).tiff

All of the controls in the iTunes Cover Flow interface are drawn with OpenGL textures. That includes the buttons, scrollbar and slider. These are absolutely not NSControls, which can only draw in a Quartz graphics context. If you want users to interact with your NSOpenGL view, you’re going to have to start from scratch with your controls. Don’t let this scare you away from using OpenGL views for your interface. They’re fast fast fast and if your app is displaying lots and lots of images, especially in a full screen situation with animation, they’re the way to go.

Leopard’s LayerKit (Core Animation Layers) technology advertises mixed Quartz and OpenGL drawing, but I just have a hard time getting behind that idea. More on this thought another time.

Using NSView

The subject of creating controls for an OpenGL view is a little out of the scope of this tutorial. Again, I will come back to the issue of OpenGL views in the not-too-distant future.

Working with NSView will let us touch on the very basics of what we need to make a control. For really specialized, one-off interfaces that you can add to your window’s view hierarchy with no fuss, it makes a lot of sense to start here.

As I mentioned before, there are three parts to a control. Let’s make a to-do list for our class design. Our view must be able to handle:

  1. Drawing
  2. Event Handling
  3. Communication with other parts of your app

We’ll handle number one with NSView and number two with its superclass, NSResponder. For number three, we’ll use Cocoa bindings.

What Are we Controlling?

Before we subclass anything, we need to figure out what our control will control. The example project contains a control for adjusting the values of a twirl distortion CoreImage filter. To start the design, I looked at the documentation for CITwirlDistortion. It lists the three attributes of the filter: center postition, radius and angle. I decided to create a circular design for controling all three attributes at once. This seemed to be a nicer interface than providing three sliders. Of course this design has some usability issues that will be obvious once you play around with it a bit, but it’s fun to experiment.

KDTwirlDistortionControl has this design:

conrolDiagram.jpg

It’s basically a circle. Users can drag on the main gray part of the circle to adjust its radius - this will change the radius attribute of the filter. Dragging on the blue center point will change the position of the circle and the center position attribute of the filter. Finally, dragging the small black circle around its perimeter will adjust the angle attribute of the filter.

We have an interface design. Time for code.

Defining the NSView subclass

Our NSView subclass, KDTwirlDistortionControl, needs to keep track of three rectangles (pictured in the diagram) that it will use for drawing and hit detection. These rectangles will also be used to calculate the adjustment values of the filter. It declares the following ivars for the rectangles in the header file:

// drawing and hit detection
NSRect	mPositionControlRect;
NSRect	mAngleControlRect;
NSRect	mRadiusControlRect;

The control also contains 4 float values that can be bound to. It does this by declaring the following ivars and their getters and setters:

// the values we bind to
float	mPositionX;	// a value between 0 and 1
float	mPositionY;	// a value between 0 and 1
float	mRadius;	// a value between 0 and 1
float	mAngle;		// a value between 0 and 6 radians

The getters and setters will always return normalized values for these ivars, which are calculated based on the positions and sizes of the rectangles in the view’s coordinate system. Look in the code for more details about this part of the implementation.

With the getters and setters in place, our control can communicate changes to these values to other classes with no more code. We can check number three off of our to-do list. Thanks Cocoa.

The last thing our control needs to manage is its state. There are four possible states in our design:

  1. the control is inactive
  2. the position rectangle is active
  3. the radius rectangle is active
  4. the angle rectangle is active

We’ll use this information when we draw to give the user feedback about what they are adjusting. We’ll also use it in our mouse drag handler so that we know which rectangle the user is adjusting. To define the states of the control, create an enum type like this:

typedef enum
{
	kKDTwirlDistortionControlState_Inactive = 0,
	kKDTwirlDistortionControlState_AngleControlActive,
	kKDTwirlDistortionControlState_RadiusControlActive,
	kKDTwirlDistortionControlState_PositionControlActive

}KDTwirlDistortionControlState;

The view subclass declares an ivar that it will use to keep track of its current state:

// state
KDTwirlDistortionControlState		mControlState;

This is all the data we need for our control. Now we will use NSView’s drawing and NSResponder’s event handler methods to implement the control.

Drawing

All we need to do to draw in an NSView subclass is to override the following NSView method:

- (void)drawRect:(NSRect)theRect;

To keep things organized, KDTwirlDistortionControl uses separate drawing methods for each part of the control. It also creates a separate method to lay out the rectangles in the view. It declares them as private methods in a category in the .m file:

@interface KDTwirlDistortionControl (Private)
- (void)drawPositionControlInContext:(CGContextRef)theContext;
- (void)drawAngleControlInContext:(CGContextRef)theContext;
- (void)drawRadiusControlInContext:(CGContextRef)theContext;
- (void)layoutControls;
@end

KDTwirlDistortionControl’s drawRect method looks like this:

- (void)drawRect:(NSRect)theRect
{
 CGContextRef aCGContextRef = [[NSGraphicsContext currentContext] graphicsPort];

 // draw the background
 CGContextSetRGBFillColor(aCGContextRef, 0.8, 0.8, 0.8, 1.0);
 CGRect aCGBackgroundRect = *((CGRect*)&theRect);
 CGContextFillRect(aCGContextRef, aCGBackgroundRect);

 // layout the controls
 [self layoutControls];

 // draw the radius control rect
 [self drawRadiusControlInContext:aCGContextRef];

 // draw the position control rect
 [self drawPositionControlInContext:aCGContextRef];

 // draw the angle control rect
 [self drawAngleControlInContext:aCGContextRef];
}

If you look at the draw methods, you’ll notice that before they draw, they check the control state ivar to determine what color they will use to draw. This gives the user feedback about the state of the control. You might also notice that the control uses Quartz2D drawing commands. Your control could just as well use Cocoa’s NSBezierPath class to draw itself. It’s good to be familiar with both drawing APIs. There might be cases where you will notice the performance difference, so it’s nice to be able to fall back on Quartz for drawing straight into the view instead of using another object.

We can check number 1 off of our to-do list. The drawing is finished. One more.

Event Handling

NSView is a subclass of NSResponder. The mechanism of the view hierarchy ensures that any view you add to it will be placed in the application’s “Responder Chain”. The window will use the view hierarchy to deliver mouse events to the view that the user clicked on. Keyboard events will be delivered through the responder chain. It’s up to the view to implement the NSResponder event handlers that it is interested in.

Most mouse events will be sent automatically to the appropriate view, but there is one step that a view must take to receive keyboard events. The keyboard events go to the “firstResponder” first and then travel through the responder chain. If your view is to receive keyboard events it must accept first responder status. To do this, the view needs to override the NSResponder method:

- (BOOL)acceptsFirstResponder

to return YES. By default it returns NO.

Here’s a list of the most common mouse event handlers that a custom view can impelment:

- (void)mouseDown:(NSEvent*)theEvent;
- (void)mouseDragged:(NSEvent*)theEvent;
- (void)mouseUp:(NSEvent*)theEvent;
- (void)mouseMoved:(NSEvent*)theEvent;
- (void)mouseEntered:(NSEvent*)theEvent;
- (void)mouseExited:(NSEvent*)theEvent;

And common keyboard event handlers:

- (void)keyDown:(NSEvent*)theEvent;
- (void)keyUp:(NSEvent*)theEvent;

NSEvent

All of the event methods are going to pass us an NSEvent object that describes the event. If it’s a mouse event, we can ask the event for the location of the mouse point using the NSEvent method:

- (NSPoint)locationInWindow;

This is going to give us the location of the mouse event in the window (duh). We need to convert this point to a coordinate in our view’s internal coordinate system. We’ll use NSView’s method

- (NSPoint)convertPoint:(NSPoint)thePoint fromView:(NSView*)theView;

If the event is a keyboard event, we can ask the NSEvent object for a string of characters associated with the event with the following methods:

- (NSString *)charactersIgnoringModifiers;
- (NSString *)characters;

Another useful bit of information we can get from NSEvent is whether or not a modifier key is associated with the event. Use the method

- (unsigned int)modifierFlags

to get the current modifier flags, which you can find documented in the list of NSEvent’s constants. Check this against the modifier flag you are looking for. This bit of code is checking for the command key:

if(([theEvent modifierFlags] & NSCommandKeyMask) != 0)
{
 // do something special
}

Check the NSEvent documentation for more information on getting neat things like tablet data.

Ok, Let’s get back to our control.

Mouse Down

When our view receives a mouse down, we want to check to see if the mouse point is inside any of the rectangles. There’s a handy function we can use to check a point struct against a rectangle struct:

BOOL NSPointInRect(NSPoint thePoint, NSRect theRect);

KDTwirlDistortionControl’s mouse down method checks all of the rects against the mouse point. If it determines that a click has occured inside any of its rects, it sets the control state to indicate which rectangle has been hit, tells itself to redraw and returns. Here’s an excerpt:

// down on the position control rect
if(NSPointInRect(aMousePoint,mPositionControlRect))
{
 mControlState = kKDTwirlDistortionControlState_PositionControlActive;
 [self setNeedsDisplay:YES];
 return;
}

If none of the rectangles have been hit, the view centers the position rectangle around the mouse point. This lets user click the background to set the position - hint: this feature comes in handy if you happen to drag it to an unusable position, like I said there are problems in this design ; )

Mouse Dragged

This might be the most important method our class implements. The mouse drag is going to change the values of our active rectangle. It’s also going to call the setter methods of our control’s float values, which will automatically trigger notification of a value change to any class that is bound to the control.

The first thing the mouse dragged handler does is check the control state to determine which rectangle the user is adjusting. It will then perform the calculations it needs to make adjustments to both the rectangle and the associated filter value. It sets the value, tells itself to redraw and returns. Here’s an excerpt:

// dragging the position control rect
if(mControlState == kKDTwirlDistortionControlState_PositionControlActive)
{
 // center the rect around the mouse point
 float aNewXPosition = aMousePoint.x-mPositionControlRect.size.width*.5;
 float aNewYPosition = aMousePoint.y-mPositionControlRect.size.height*.5;
 mPositionControlRect.origin = NSMakePoint(aNewXPosition, aNewYPosition);

 [self setPositionX:aNewXPosition];
 [self setPositionY:aNewYPosition];
 [self setNeedsDisplay:YES];

 return;
}

Mouse Up

The mouse up simply sets the control state to be inactive and tells itself to redraw.

Key Down and Key Up

The key down event handler looks for arrow key presses. When an arrow key is pressed, it adjusts the origin of the position rectangle a few points in the appropriate direction. It also sets the current control state so that the position rectangle is active and redraws. Here’s an excerpt from the method. It uses a switch statement to find the arrow keys:

case NSUpArrowFunctionKey:
   mControlState = kKDTwirlDistortionControlState_PositionControlActive;
   mPositionControlRect.origin.y = mPositionControlRect.origin.y+5;
   [self setPositionY:mPositionControlRect.origin.y];
   [self setNeedsDisplay:YES];
break;

The key up method also checks for arrow keys. If the event is from one of the arrow keys, it resets the control state to inactive and redraws.

Event handling was the last thing on our to-do list. Scratch it off, we’re done. We have a control.

Hi Asimo! Nice Hair-do!

Picture 21.png

The KDTwirlDistortionControl example is very specific to the kind of data it is controlling and it could be used with any Core Image filter that has the same attributes, despite its name.

In a future post, I will explore ways to abstract the design a little more to make the control more useful in more situations. This will be necessary for designing OpenGL controls and could be useful if you’re using CALayers as the basis for a control. I haven’t had a chance to work with CALayers, but I notice that they’re not descendants of NSResponder. Odd for such a view-like class.

Resources

The Example Project

Cocoa Event handling Guide

The Responder Chain

Cocoa drawing guide

Quartz2D Programming Guide

NSView - Working With the View Hierarchy

Cocoa Bindings

More advanced bindings examples: mmalc’s bindings examples, look at graphics bindings

Why I Say ‘NO’ to Autoresizing

Tuesday, February 12th, 2008

Cocoa does so much so well, but like any framework, it has its quirks. It can be difficult to decide when it’s time to put your foot down and say “No. Thanks. I’ll take over from here.” For me, one of those moments came when I had finally had it with NSView’s autoresizing behavior. Like a self-doubting girlfriend, convinced that I must be doing something wrong, I let it go for a long time. Eventually I had to face the truth. My views were not where I expected them to be and it wasn’t because my expectations were unreasonable. Autoresizing just wasn’t working.

Autoresizing

NSView has an “autoresizing” mechanism. It’s a layout tool that lets the developer configure how a subview will resize and reposition itself when its superview’s frame rectangle changes. For example, let’s say that we have a window with a button in the bottom right corner and we want it to stay there when the user resizes the window.

Picture 2.png

The button is a subview of the window’s “content view”, so we’ll configure the button’s autoresizing behavior. The button is fine being positioned at the bottom of the window, but we want it to move along the x-axis as the window’s width changes. To do this, we’ll use the autoresizing mask constant NSViewMinXMargin. According to the docs,

If set, the view’s left edge is repositioned proportionally to the change in the superview’s width. Otherwise, the view’s left edge remains in the same position relative to the superview’s left edge.

If we want to configure the view programmatically, we’ll first let the superview know that it needs to trigger the autoresizing, then we’ll configure the subview’s autoresizing mask.

[oWindowContentView setAutoresizesSubviews:YES];
[oButton setAutoresizingMask: NSViewMinXMargin];

Or we can use the nifty visual editor in Interface Builder to set “springs and struts”:

Picture 41.png

The Problem

The example above works great, but it doesn’t take much to get autoreszing to misbehave. I’m going to make a view and give it one subview. I want the outer view to adjust its width and height with the window and the inner view to stay its original size, but to remain centered in its superview. To achieve this layout, I’ll use the following code:

// turn on autoresizing
[oWindowContentView setAutoresizesSubviews:YES];
[oOuterView setAutoresizesSubviews:YES];

// set autoresizing mask
[oOuterView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
[oInnerView setAutoresizingMask:(NSViewMinXMargin | NSViewMinYMargin |
		 		 NSViewMaxXMargin | NSViewMaxYMargin)];

This is what happens:

Everything is fine for a little while, the inner view stays centered. But at one point, after the outer view’s height becomes smaller than the inner view’s height and I expand the window back out, the inner view’s Y margin is lost and its original setting is broken. The inner view is stuck to the top of the outer view’s frame.

This is a very simple example. In real life, view hierarchies are much more complex. We have views inside split views inside other split views next to other views containing buttons, text fields and sliders and they all have to know what to do when the user resizes the window or drags a divider. Behavioral quirks like the one in the video become harder and harder to live with as our interfaces grow.

Taking Control

I sometimes think that designing a dynamic layout is a bit like choreographing a dance routine. What should I do when the dancers just can’t get it right, no matter how many times we’ve been over it? I could instruct them to just stay still when they forget what to do and to jump back in when they’re ready. I could hire someone to stand on the sidelines during the performance, reminding them of their next move when they look lost. Another option is to just replace them with dancers who listen and remember.

I spoke with an Apple engineer at the last WWDC about the problems I was having with autoresizing. I can still see the look on his face when he realized what I was talking about. It was as if he was thinking, “Oh yeahhhhh THAT…” Yeah. That. I asked if the NSView people had fixed That in Leopard. He confirmed that no, they had not and suggested that I set a minimum size for the superviews to prevent the situation. He also asked me what I thought they could do to anticipate resizing past certain sizes.

The idea of setting a minimum size for the superview is the common fix for this. You see it most often with split views, where superviews are very likely to be sized smaller than their subviews. It seems to be an accepted practice to restrict split view divider dragging past some arbitrary point when there is no real usability or layout-related reason to do so. The behavior I see time and again is that just before the split view divider reaches the subview’s amnesia-inducing point, it’ll automatically jump closed and when it’s dragged back out, it’ll jump back. This is bad interaction design. It’s a work-around that has nothing to do with what is good or bad in terms of “user experience”. In fact, it creates a bad user experience. Things shouldn’t be jumping around under the mouse, especially in the middle of a split view divider drag when the user is expecting some measure of control over the layout.

So what could NSView do to prevent this behavior? It could cache margins or at least the proportions we’re trying to preserve. That way, it will always have the correct value to use for its calculations. Seems kind of easy to me.

For the time being, we’re stuck with this behavior and we have to either set minimum sizes for superviews, pollute our application code with controllers to monitor and adjust the layout, or subclass NSView and all of its subclasses and define our own resizing behavior. None of these situations is ideal. The first results in less dynamic layouts and less enjoyable interactions, but it’s simple. The second is just dirty and the last is a pretty big undertaking considering how many subclasses NSView has. I’m sure there are other creative solutions we could consider. You can probably guess what I do from the title of this post. I subclass and bypass the whole autoresizing mechanism. The only time I turn on autoresizing is for the view that’s at the very top of the hierarchy so that it resizes with the window. My solution has its own issues, but they’re pretty easy to deal with and I get an amazing dance troupe that can handle even my most avant-garde choreographic efforts.

A zip file containing the Xcode project I use in the video can be downloaded here.

UPDATE (02-16-08)

I was just thinking about what the bug really is. I don’t think that it’s an issue of simply cacheing a value anymore. There’s something else going on.

If we look at NSViewMinYMargin. Again from the docs:

If set and the superview is not flipped, the view’s top edge is repositioned proportionally to the change in the superview’s height.

I think it has to do with the change calculation. You’ll notice in the video, it didn’t happen each time i shrank the superview. I think that when you’re resizing fast enough, it’s possible that the notification of the frame change is a little behind and when it goes to make the calculation, the change is 0. That doesn’t explain why it stays “stuck”, but it is possible to “unstick” it if you keep resizing over and over. When it unsticks, the proportions are off, so that it’s no longer centered, so there’s something weird there, too. Very very weird.