Re: guidelines for using autolayout with NSScrollView?
Re: guidelines for using autolayout with NSScrollView?
- Subject: Re: guidelines for using autolayout with NSScrollView?
- From: Kyle Sluder <email@hidden>
- Date: Sat, 16 Mar 2013 15:05:59 -0700
On Mar 16, 2013, at 12:46 PM, Chuck Soper <email@hidden> wrote:
> Are there similar notes on how to use autolayout with NSScrollView? Does
> anyone know the specific details on what Apple recommends? For example,
> are both mixed and pure autolayout approaches okay with NSScrollView? (The
> mixed or pure autolayout terminology comes from the UIScrollView notes -
> link above.)
There is no official recommendation, but I have had a *lot* of experience in this area. And I've been meaning to write a blog post about this as a follow-up to my Xcoders presentation on auto layout, so consider this a draft!
The biggest issue with using auto layout in an NSScrollView's document view is that NSScrollView itself is completely unaware of auto layout. Thus, it relies on the behavior provided by setting translatesAutoresizingMaskIntoConstraints=YES on its subviews so it can continue to use -setFrame: to position them even if auto layout is turned on for the window.
At this point it's worth noting another difference between NSScrollView and UIScrollView: UIScrollView directly manipulates its own bounds.origin to perform scrolling of its entire subview hierarchy. The contentSize property dictates the size of the scrollable region.
NSScrollView, on the other hand, doesn't actually do any scrolling on its own: it delegates that responsibility to an NSClipView. The scroll view positions the clip view and scrollers (if they are visible) using -setFrame:. Rather than exposing a contentSize property, NSClipView observes the frame of _one specific subview_ called its documentView. This view's frame.size becomes the equivalent of UIScrollView's contentSize.
In order to perform scrolling correctly, the documentView's frame.origin must lie at (0,0) in the clip view's bounds coordinate system. NSClipView, like NSScrollView, is unaware of auto layout, so it uses -setFrameOrigin: to put the document view at (0,0). If auto layout gets turned on for the window, this position gets turned into a pair of constraints with a priority of 1000 (required).
Two more constraints will be synthesized to define the width and height of the document view. These constraints are the problem. In either direction, one of two kinds of constraints will be generated, depending on the documentView's autoresizing mask for that direction:
1. If the view is stretchable in that direction, a constraint will be installed relating the opposing edge of the documentView to the edge of the superview.
2. If the view is not stretchable in that direction, a constraint will be installed dictating the absolute value of the documentView's frame.size in that dimension.
Like all autoresizing-mask constraints, these constraints are required (priority 1000). Because the entire constraint system is solved at once, it should be intuitive that any constraints that attempt to influence the size of the documentView will conflict with either the constraints installed by the clip view on the documentView and/or with the constraints installed by the scroll view on the clip view.
So we have a dilemma. We need to somehow break the bidirectionality of the relationship between the clip view and the documentView. There is no straightforward way to express this using the constraints API, but it is indeed possible without resorting to mucking with private internal details.
In other words, we want to somehow run layout of arbitrary constraints on our documentView's subtree and retrieve the resultant frame of the documentView without involving the documentView itself in our constraint system. Once we have the right values, we can use -setFrameSize: on the documentView; the clip view will notice and it will update its scrollable area.
The way we accomplish this is to install another view in our subtree and define all our constraints relative to _that_ view. I'm going to call this the adaptor view. The documentView installs constraints to keep the adaptor view's top and leading margins equal to zero, but critically it does NOT install any constraints on the trailing or bottom edges. This leaves the adaptorView's width and height free to be defined by its content's layout.
The documentView signs up for frame change notifications from the adaptor view. Whenever it changes its frame, the documentView calls [self setFrameSize:] with the same size. Then the clip view hears about this, and the scroll view reflects the correct document size. For this to work, the documentView's autoresizing mask should be set to width and height NON-stretchable, that way when the clip view resizes (perhaps during window dragging) it doesn't resize your documentView.
If you're laying out the contents of your scroll view in IB, the most convenient approach is to make the adaptor view the only subview of your documentView, and to add all your widgets as subviews of the adaptor view. The bonus here is that if your constraints are insufficient to fully specify the frame of the adaptor view, IB will doggedly insist on adding margin or fixed-size constraints until you fix your layout. Yes, we have turned one of IB's most annoying habits into a feature!
If you're generating all your constraints in code, however, it might be just as convenient to make your adaptor view a _sibling_ of all your widgets and to call -setHidden:YES on it, thus removing a level of hierarchy between the documentView and your widgets.
>
> I think that autolayout is a great technology.
Me too, but it will be even better when AppKit fully supports it. All the logic I described above could be added to NSClipView, and suddenly NSScrollView would be constraint-aware.
Let's see what 10.9 brings.
--Kyle Sluder
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden