Why I Say ‘NO’ to Autoresizing
Tuesday, February 12th, 2008Cocoa 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.

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”:

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.