Getting NSWindow resizing right in an auto layout world - macos
I'm having problem getting vertical window resizing working with an auto layout scroll view.
What I Want
I would like to replicate, as closely as possible, my app's current window resizing behaviour. The width of the window is flexible, but the height of the window should generally track the height of the contents. Specifically:
Normally, the window automatically resizes its height to exactly
match its contents (with the exception of #2).
The user can choose to manually resize the window so it's smaller than its
contents, in which case the imbedded scroll view will scroll the
contents. Once made shorter, the window remains that height until manually
resized or the contents becomes equal or smaller than the window again.
If the contents grows to the point that the bottom of the window
bumps into the dock or screen bounds, the window's height is pinned
to that height, just as if the user has resized it.
What I've Got
I created a window with a set of representative subviews that mimic the basic needs of my app. The window's hierarchy is simple:
Window
NSView (contentView)
NSScrollView
NSClipView (NSScrollView.contentView)
NSView (NSScrollView.documentView)
A bunch of standard and custom subviews with constraints
You can download the test project here (macOS 10.12/Xcode 8): http://mbx.cm/t/4FUGY
I've organized the various subviews so they have a flexible width, but the constraints define exactly one possible height. The parent scroll view fills the content view of the window.
The auto layout stuff works great. The window automatically resizes to match the size of the contents. If the height of the contents changes, the height of the window changes to match. Awesome.
What I Can't Get to Work
I have had no luck getting NSWindow to let me manually resize its height. The resize indicator (when hovering over the edge) shows that I can alter the window's width, but not its height.
I initially thought "Oh, that's got to be a compression resistance priority in one of the scroll views." But I can find no combination of compression resistance or hugging priorities that will alter this behavior. I've tried setting the priorities of the scroll view itself, and on the documentView (which makes no sense to me, but I tried anyway). I tried the values 749, 499, 49, and 1 in every combination I could think of.
I've searched for every problem that seemed released to this, but most of the posted questions seemed to be addressing different problems.
I added a "Dump" button to log the vertical constraints. Everything listed appears to be as expected, except for a handful of NSAutoresizingMaskLayoutConstraint objects that I don't understand. However, these appear to be constraints between the document view and the clip view, so I assume those are automatically created and aren't part of the problem.
<NSLayoutConstraint:0x608000082580 PartBoxView:0x608000140840'Part B'.height == 96 (active)>
<NSLayoutConstraint:0x608000083de0 V:[PartBoxView:0x608000140840'Part B']-(0)-| (active, names: '|':NSView:0x6080001212c0 )>
<NSLayoutConstraint:0x608000083e80 V:[PartBoxView:0x608000140370'Part A']-(NSSpace(8))-[PartBoxView:0x608000140840'Part B'] (active)>
<NSLayoutConstraint:0x608000082da0 V:|-(0)-[NSScrollView:0x6080001c10e0] (active, names: '|':NSView:0x608000121400 )>
<NSLayoutConstraint:0x608000083430 V:|-(0)-[NSView:0x6080001212c0] (active, names: '|':NSClipView:0x10040e2a0 )>
<NSLayoutConstraint:0x608000081e00 V:[NSScrollView:0x6080001c10e0]-(0)-| (active, names: '|':NSView:0x608000121400 )>
<NSAutoresizingMaskLayoutConstraint:0x650000082b20 h=-&- v=-&- NSView:0x608000121400.minY == 0 (active, names: '|':NSThemeFrame:0x102504ea0'Window' )>
<NSAutoresizingMaskLayoutConstraint:0x6500000823f0 h=-&- v=-&- NSClipView:0x10040e2a0.minY == 1 (active, names: '|':NSScrollView:0x6080001c10e0 )>
<NSLayoutConstraint:0x608000083480 V:[NSView:0x6080001212c0]-(0)-| (active, names: '|':NSClipView:0x10040e2a0 )>
<NSAutoresizingMaskLayoutConstraint:0x650000082440 h=-&- v=-&- V:[NSClipView:0x10040e2a0]-(1)-| (active, names: '|':NSScrollView:0x6080001c10e0 )>
<NSLayoutConstraint:0x608000083d40 V:|-(0)-[PartBoxView:0x608000140370'Part A'] (active, names: '|':NSView:0x6080001212c0 )>
<NSLayoutConstraint:0x608000083c50 V:[NSImageView:0x608000161bc0]-(20)-| (active, names: '|':PartBoxView:0x608000140370'Part A' )>
<NSLayoutConstraint:0x608000083bb0 V:|-(20)-[NSImageView:0x608000161bc0] (active, names: '|':PartBoxView:0x608000140370'Part A' )>
<NSLayoutConstraint:0x608000083660 NSImageView:0x608000161bc0.height == 64 (active)>
I'm hoping someone with auto layout experience can tell me how to get #2 working. I'm pretty sure #3 will require some custom code, but #2 is the big hurdle.
The Background
I'm in the process of giving my app a major facelift. It's a pretty big app, so I started by creating a set of test projects to test out some of the new UI and techniques.
The first big undertaking is converting everything to auto layout. Most of it looks like it will be fairly smooth, and I'm looking forward to some of the many benefits of auto layout.
There's no completely automated way to achieve your goal #2. You want two different modes. In one mode, the window is always made large enough to accommodate the content. If it grows, the window grows; if it shrinks the window shrinks. In the other mode, the window is allowed to be smaller than required to fit the content because the user resized it that way. In that case, if the content grows, the window doesn't grow; if it shrinks, the window doesn't shrink unless the content gets small enough that it all fits and then it switches back to the first mode.
Auto layout doesn't really do modes like this, at least not automatically. You will have to detect the mode changes and programmatically modify the constraints to implement the two behavior modes.
You have apparently created constraints between the document view and the clip view to keep their tops and bottoms coincident. That essentially forces the clip view and then the scroll view to be as large as the document view. The scroll view will never scroll because it will never be too small to show all of the content.
I think you may want two constraints for the bottom spacing. One constraint will be an inequality at required (1000) priority. You want to express that the clip view should never be bigger than the document view. The document view's bottom can be greater than or equal to the clip view's bottom, but never less than.
The second bottom spacing constraint will be an equality (with 0 constant, as now) but with priority slightly less than NSLayoutPriorityWindowSizeStayPut (500). This expresses that you want the clip view and scroll view to be large enough to fit the content, unless that would force the window to grow or prevent the user from shrinking it.
The problem is that if the window is large enough to fit the content and then the content grows, that won't force the window to grow. What I've described implements the second mode.
You could try to implement the first mode by setting the second constraint's priority higher. The problem then is that the user won't be allowed to resize the window. You're back to your current situation.
What I think you'll need to do is notice when the content resizes, by observing the document view's NSViewFrameDidChangeNotification. Be sure to tell the view to post that by setting its postsFrameChangedNotifications property to true. When the frame changes, if you think you should be in the first mode, set the second constraint's priority higher, call -layoutIfNeeded on the window, and then set the priority back down. I think you may need to defer setting the priority down to the next turn of the event loop because it's not clear that you'll get the notification after the clip view has, so maybe use GCD to schedule that.
So, how do you know which mode you should be in? I'm not entirely sure. I think it will work for the window delegate (often its controller) to implement -windowDidEndLiveResize: to know when the user has finished resizing the window. I think the user resizing it will be a live resize while programmatically resizing it or auto layout resizing it won't be.
If it was the user who resized the window, you then need to know if the user grew the window so all of the content fits or if the user sized it smaller than that. For that, you could compare the height of the document view's bounds against the clip view's documentVisibleRect.
Ken,
Thanks for the thorough and thoughtful response.
You have apparently created constraints between the document view and the clip view to keep their tops and bottoms coincident.
Well, I didn't, but IB certainly did. ;)
So the first step was to edit the clip view constraints, changing the clipView.bottom-0-documentView.bottom constraint from "equals 0" to "less than or equal to 0". That permits the clip view to be (vertically) smaller than the document view, ultimately allowing the user to resize the window vertically.
I then started with your other suggestions, adding some additional constraints to pin the height to the document and either modifying its active property or changing its priority.
Ultimately, however, I went a slightly different route. The problem is that when you ask the window's contents to grow a lot, or when it's close to the bottom of the screen, its behavior is ... well, weird.
Instead, I created a "sticky" mode for the window. When set, and the document view grows, I manually calculate a new frame for the window. I do this because I can control how the window resizes when it is near the bottom, and/or top, of the screen.
Warning
I discovered the hard way that there's a hidden danger to all of these techniques. The NSViewFrameDidChangeNotification is sent whenever the frame is resized. This can happen during auto layout. If you observe this notification and immediately adjust the window size, content size, or constraints, auto layout gets very upset and throws nasty "circular" and "recursive" layout warnings (it also sometimes fails to resize properly). The solution was to simply wrap up the window size fix in a block and queue it to execute on the main thread, after all of the auto layout logic has finished.
Here's the finished, working, test project (with comments and notes): http://mbx.cm/t/Zjdml
Here's the relevant code:
#interface ViewController ()
{
BOOL windowSizeSticky; // a change in the content size should resize the window to match
}
- (void)documentSizeChangedNotification:(NSNotification*)notification;
#end
#implementation ViewController
- (void)dealloc
{
self.view.window.delegate = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Enable the document view to post size change notifications
NSView* docView = self.scrollView.documentView;
docView.postsFrameChangedNotifications = YES;
// Subscribe to those changes
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(documentSizeChangedNotification:)
name:NSViewFrameDidChangeNotification
object:docView];
// Queue up an initial evaluation so windowSizeSticky is set correctly
dispatch_async(dispatch_get_main_queue(), ^{
[self documentSizeChangedNotification:nil];
});
}
- (void)viewWillAppear
{
// Make this controller the window's delegate
self.view.window.delegate = self;
[super viewWillAppear];
}
- (void)windowDidEndLiveResize:(NSNotification *)notification
{
// Whenever the user resizes the window, reevaluate the windowSizeSticky mode
NSView* documentView = self.scrollView.documentView;
NSClipView* clipView = (NSClipView*)(documentView.superview);
NSRect docVisible = clipView.documentVisibleRect;
NSRect docFrame = documentView.frame;
// Update the "sticky" mode depending on whether the window now displays all, or only a portion, of the contents
windowSizeSticky = (docVisible.size.height==docFrame.size.height);
}
- (void)documentSizeChangedNotification:(__unused NSNotification *)notification
{
NSView* documentView = self.scrollView.documentView;
NSWindow* window = documentView.window;
if (!window.inLiveResize) // Suppress this logic while the user is manually resizing the window
{
dispatch_async(dispatch_get_main_queue(), ^{
// Do this the next time the main loop is idle
// This notification can be sent during auto layout, and we don't want to attempt to resize
// the window in the middle of an auto layout calculation.
// The geometry of the document view has changed; check to see if the window needs resizing
NSClipView* clipView = (NSClipView*)(documentView.superview);
NSRect docVisible = clipView.documentVisibleRect;
NSRect docFrame = documentView.frame; // The doc's frame is in the clip view's coordinate system
if (docVisible.size.height==docFrame.size.height)
{
// All of the document is (vertically) visible in the clip view
// That means the window is displaying all of its contents
// Whenever this happens, switch to "sticky" mode so future changes in content will make the window grow
windowSizeSticky = YES;
}
else if (windowSizeSticky && docVisible.size.height < docFrame.size.height)
{
// The content is now taller than the view port of the scroll view & the window is "sticky"
// Try to make the window taller so all of the content is exposed
NSRect windowFrame = window.frame;
CGFloat addHeight = docFrame.size.height-docVisible.size.height;
NSRect contentRect = [window contentRectForFrameRect:windowFrame];
contentRect.size.height += addHeight;
// Calculate an ideal window frame, then adjust the existing frame so it's as close as we can get
NSRect targetFrame = [window frameRectForContentRect:contentRect];
CGFloat deltaY = targetFrame.size.height-windowFrame.size.height;
if (deltaY >= 1.0)
{
// The window needs to be taller
// Make it tall enough to display all of the content, keeping its title bar where it is
windowFrame.origin.y -= deltaY;
windowFrame.size.height += deltaY;
// Screen bounds check...
NSRect visibleFrame = window.screen.visibleFrame;
if (visibleFrame.origin.y>windowFrame.origin.y)
{
// The bottom of the window is now below the visible area of the screen
// Move the whole window up so it's back on the screen
windowFrame.origin.y = visibleFrame.origin.y;
if (visibleFrame.origin.y+visibleFrame.size.height < windowFrame.origin.y+windowFrame.size.height)
{
// The top of the window is now off the top of the screen
// Shorten the window so it's entirely within the screen
windowFrame.size.height = visibleFrame.size.height;
// This also means "sticky" mode is off, since we had to size the window to something smaller
// than its contents.
windowSizeSticky = NO;
}
}
[window setFrame:windowFrame
display:NO
animate:NO/* be consistent; constraints doesn't animate when getting shorter */];
}
}
// else { window is not sticky OR its contents doesn't exceed the height of the window: do nothing }
});
}
}
#end
Related
Centering view in scroll view
I have an NSView embedded inside a NSScrollView. When changing the magnification in my app I'm changing the view's frame by adjusting the constraints and calling layoutSubtree:. Now I'd like to center the same area of the view after changing the embedded view's size. The following bits of code should be doing the trick - the scrollers actually look updated, i.e. they expand and stick to the same position. However the scroll view doesn't seem to know about the changed position. When starting to scroll the (updated) view manually the scroll bar positions 'jump' from the position changed via code to the actual position that's reflecting the view's scroll position. It feels like the inverse of reflectScrolledClipView: is required after setting the scrollers via code.. Pseudocode: double hScrollPos = NSScrollView.horizontalScroller.doubleValue; double vScrollPos = NSScrollView.verticalScroller.doubleValue; [view updateFrameSizeAndStuff]; [view invalidateIntrinsicContentSize]; // this seems to early though: NSScrollView.horizontalScroller.doubleValue = hScrollPos; NSScrollView.verticalScroller.doubleValue = vScrollPos;
NSScrollView with auto layout not resizing until first manual window resize
I've got an OSX Cocoa app that has been built programatically (i.e., not with a NIB/XIB), which I'm trying to lay out using auto layout - but I'm getting some odd behaviour when the window first displays. My main content is an NSView that holds has a collection of 100 NSButtons as subviews, laid out vertically. The buttons are all constrained relative to the NSView and each other; both the NSView and all the NSButtons have translatesAutoresizingMaskIntoConstraints=NO set. I believe the code for the content view is good (i.e., no ambiguous layouts, etc), because if I set the main window's contentView to the NSView, the buttons display as expected. However, if I set the main window's contentView to be an NSScrollView, and set the documentView of the NSScrollView to be the NSView, I get display problems. On first display, I get a blank window - no scroll bars, nothing: The NSScrollView has translatesAutoresizingMaskIntoConstraints=NO. For debug purposes, I've also set the background colour of the NSScrollView to blue so that I can confirm what is being laid out where - but there's no blue shown anywhere. But, as soon as I resize the window, the layout kicks in, and I get an NSScrollView the full size of the main window, with blue background, and scrollbars as expected: I've read some references that suggest the problem is the lack of constraints on the clipView that is part of the NSScrollView. On that basis, I've tried setting up constraints binding [NSScrollView contentView] to [NSScrollView documentView] in the vertical and horizontal directions (with constant 0, multiplier 1, on the left, right, top and bottom). When I do this, the NSScrollView is now visible on first display, but it's the wrong size. The scroll doesn't scroll the full height of the internal content - the scrollable content scrolls as if it is the same size as the visible window. Lastly, the content overlaps the titlebar of the window: Again, as soon as I resize the window, the constraints kick in, and the window displays as I'd expect (see the previous screenshot). So, I take it the extra constraints don't hurt, but they don't seem to be adding anything, either. Further confusing matters - if I leave the buttons off altogether, and just use an empty NSView with no subviews as the content view, I get a full window of blue on startup, as I'd expect. So - what's going on here? It feels like I'm missing a call to force the evaluation of constraints on the buttons; is that the case, or is something else going on here? For those interested - here's my sample code. It's not Objective C - it's Python - but the language binding can convert Python method names into Objective C messages; the mapping to native ObjectiveC API should be obvious: app = NSApplication.sharedApplication() app.setActivationPolicy_(NSApplicationActivationPolicyRegular) main_window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_( NSMakeRect(100, 100, 640, 480), NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask, NSBackingStoreBuffered, False) scrollview = NSScrollView.alloc().init() scrollview.setHasVerticalScroller_(True) scrollview.setHasHorizontalScroller_(True) scrollview.setAutohidesScrollers_(True) scrollview.setBorderType_(NSNoBorder) scrollview.setTranslatesAutoresizingMaskIntoConstraints_(False) scrollview.backgroundColor = NSColor.blueColor() container = NSView.alloc().init() container.setTranslatesAutoresizingMaskIntoConstraints_(False) buttons = [ NSButton.alloc().init() for b in range(0, 100) ] for i, button in enumerate(buttons): button.setBezelStyle_(NSRoundedBezelStyle) button.setButtonType_(NSMomentaryPushInButton) button.setTitle_(get_NSString('Button %s' % i)) button.setTranslatesAutoresizingMaskIntoConstraints_(False) container.addSubview_(button) if i == 0: container.addConstraint_(NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( button, NSLayoutAttributeTop, NSLayoutRelationEqual, container, NSLayoutAttributeTop, 1, 50, )) else: container.addConstraint_(NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( button, NSLayoutAttributeBottom, NSLayoutRelationEqual, buttons[i-1], NSLayoutAttributeBottom, 1, 50, )) container.addConstraint_(NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( button, NSLayoutAttributeLeft, NSLayoutRelationEqual, container, NSLayoutAttributeLeft, 1, 50, )) container.addConstraint_(NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( button, NSLayoutAttributeRight, NSLayoutRelationEqual, container, NSLayoutAttributeRight, 1, -50, )) container.addConstraint_(NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( buttons[-1], NSLayoutAttributeBottom, NSLayoutRelationEqual, container, NSLayoutAttributeBottom, 1, -50, )) scrollview.setDocumentView_(container) main_window.setContentView_(scrollview) main_window.makeKeyAndOrderFront_(None) app.activateIgnoringOtherApps_(True) app.run()
I've worked out an answer - I'm not entirely happy with it, but it seems to work. There are certain Cocoa widgets that don't deal well with autolayout - in particular, I've found problems with top level NSWindows and NSTabViewItems; I'm guessing other widgets might also be affected. Essentially, these are "container" widgets that have a top level "view" that must be set. If the "contained" widget is an NSScrollView (which itself will contain other widgets), the "container" widget has difficulty establishing a size for the "contained" scroll view. The fix is to re-enable translatesAutoresizingMaskIntoConstraints for the view that will be used as the "contained" widget. In the example provided, the object scroll_view created on line 10 is the "contained" widget; the boolean value of the call to setTranslatesAutoresizingMaskIntoConstraints on line 15 should be True, not False. These problems get better with more recent versions of OS/X - Mavericks doesn't have a problem with NSWindow, but it still has a problem with NSTabViewItem. However, it doesn't seem to do any damage to turn on translatesAutoresizingMaskIntoConstraints on newer versions of OS X; all you're losing it the theoretical purity of a 100% autolayout solution.
Look at Apple's 2012 developer conference videos about Auto Layout for information about using Auto Layout in code. Simply use in Interface Builder or in code the approach I recorded in this video tutorial: How to use NSScrollView with Auto Layout This is the approach I used in this video: Window -- set delegate and IBOutlet property ScrollView -- fixed edges, no border, don't draw background documentView -- fixed edges 0, then another trailing and bottom, clipView ≥ 0 #499 and clipView ≤ 0 #501 for both trailing and bottom constraints to documentView label and text field in horizontal stack view, in vertical stack view vertical stack view fixed edges default, then another bottom, bottom ≤ default #499 and ≥ default #750 horizontal stack view leading and trailing fixed 0 label and text field align Y center to horizontal stack view text field top and bottom and trailing 2 #750, width ≥ 100, height ≥ 22 subsequent horizontal stack views leading and trailing fixed, align text field leadings
The real issue is that some of your controls are fixing the size of the view and thus for the window. For example, if you have only one view, say view1 inside your viewcontroller's view, and: set leading/trailing/top/bottom to the main view and view1.height = 300, this will make your window size to be fixed to 300 and thus not resizeable.
UIimages move to the center when going to another view
I have a drag n drop kind of app. Where you can select images and drag them anywhere on the screen! The problem I'm running into is, when you move to another view, all the images reset to the center, when I return back. For example if I press a button to take me to screen 2, all the "dragged" images I just did, will move back to the center. This only happens when I have AUTOLAYOUT enabled :( I have all my images start out in the center, so I'm guessing its something with autolayout... Any ideas ?! Here's an example of my drag image code. - (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer { CGPoint translation = [recognizer translationInView:self.view]; recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x, recognizer.view.center.y + translation.y); [recognizer setTranslation:CGPointMake(0, 0) inView:self.view]; }
Your drag and drop code is manually configuring the layout by mutating the view.center property. Once auto layout is enabled, auto layout takes responsibility for setting up the layout, so it takes exclusive responsibility for setting view.center, view.bounds, and view.frame (which is actually just calculated from center and bounds). So once auto layout is enabled, although you can still set view.center manually, auto layout will clobber whatever you do the next time it calculates the layout that satisfies the constraints you have in place. So how do you update your code to work with auto layout? If you want to use auto layout, what you need to do is modify your handlePan: method so that instead of modifying view.center it modifies whichever auto layout constraint is being used to calculate view.center. The details of this will depend on your constraint configuration. But if we assume, for example, that there is an NSLayoutContraint topSpaceConstraint that sets the view's top space to the superview, and another NSLayoutConstraint leftSpaceConstraint that sets the view's left space to the superview, then you could produce the same effect as view.center = CGPointMake(view.center.x + translation.x, view.center.y+ translation.y); by instead doing something like topSpaceConstraint.constant = topSpaceConstraint.constant + translation.y; leftSpaceConstraint.constant = leftSpaceConstraint.consant + translation.x; [view setNeedsLayout]; [view layoutIfNeeded]; The first two lines update the constraints. The last two lines cause the auto layout system to recalculate the resulting frame and apply it right away, rather than waiting until the next turn of the run loop. Alternatively, you might be able to get away with updating the frame manually when you get UIGestureRecognizerChanged events, and then making the result "permanent" by updating the constraints as shown above only once you get UIGestureRecognizerEnded event. That would be more performant, since during the many UIGestureRecognizerChanged events you'd be updating the frame directly rather than relying on the auto layout system to do it.
Dynamically resize NSWindow based on NSOutlineView height
I have read many answers to questions about dynamically resizing NSWindows and nothing has quite worked yet. I have built a 'popover' like window to be displayed form the menu bar, I couldn't use NSPopover because it isn't quite customisable enough in terms of design. My view hierarchy current looks like this: NSWindow Subclass - NSView (clears the titlebar rendering, draws popover arrow) - NSView (contentView) - NSOutlineView (main table of content) - NSView (window footer) What I need is for the window to expand and contract with items in the NSOutlineView expanding and contracting, so that the window is always precisely the correct height for the outline view, footer, and the popover arrow at the top. I have methods to calculate the required height for the outline view based on the content. So far I have been trying to recalculate the size when awakeFromNib is called on the content view controller, and then again on the delegate methods outlineViewItemDidCollapse: and outlineViewItemDidExpand:, but I am never able to resize the window correctly. I have tried lots of different ways and none worked. Is there a 'canonical' way to do this? I've seen people talking about -[NSWindow frameForContentRect:] in relation to this sort of problem, but I can't quite work out how that is needed. Maybe I am going about this completely the wrong way, I hope not though. It seems like this should be possible, it's just getting the right things in the right places. Unfortunately GUI programming is not a strong point for me. I would appreciate any ideas or solutions people have. Thanks. Edit: This was solved, partly due to the marked answer, and partly due to a few other things. The clearing NSView did not have an autoresize mask, so I set this to expand in all directions, this helped with some of the resizing issues. Also, the method I was using for calculating the height required was not entirely correct and had a few problems. With the provided answer, the way to find the required height is good for some circumstances, but I didn't have a 'root' item that could be measured. This is something that may need to be taken into account.
Here is my method, it works very well, but there are probably better than that. - (void)outlineViewItemDidExpand:(NSNotification *)notification { [self resizeWindowHeight]; } - (void)outlineViewItemDidCollapse:(NSNotification *)notification { [self resizeWindowHeight]; } - (void)resizeWindowHeight { NSRect wRect = [myWindow frame]; NSRect oRect = [myoutlineView rectOfRow:([myoutlineView numberOfRows] - 1)]; //get rect of last row CGFloat n = oRect.origin.y + oRect.size.height + 22; wRect.origin.y = wRect.origin.y + (wRect.size.height - n); wRect.size.height = n; [myWindow setFrame:wRect display:YES animate:YES]; } The number 22 is the sum of : (top window - top Outline) + height of Outline header + height of horizontal scroller + (bottom window - bottom Outline) + height of toolbar My settings in IB for this example : no horizontal scroller and no header , height of Outline = height of the contentView of the window, so 22 is the window border. If you have a toolbar or the horizontal scroller is hidden automatically, you need to add conditions in the code to check the visibility of (the toolbar and scroller) and change the height accordingly.
One approach is to record the initial height of the outline view and the initial height of the window and then, as the outline view height changes, compute the new height of the window = old height + (new outline height - old outline height). By working only in terms of changes from an initial state, it makes things more flexible. You don't have to replicate your whole layout in code as you would if you were recomputing the size of things from scratch. You can use IB to lay out the window and it should retain that layout without the code having to know much about the specifics. And you can update the layout in IB and the code still works. Otherwise, I suggest you show what you've tried and explain how it isn't right.
How do I fix the height of my view after collapsing NSSplitView?
I've got an NSSplitView with an NSScrollView in the bottom view. The problem is when I collapse, and then re-open (un-collapse) the bottom view, the height of the scroll view is beyond the height of that bottom view so the top part of the scoll view is being clipped. I've got my scroll view and my split view set to autoresize in all directions in IB. Do I need to adjust the height of that scroll view after the un-collapse or am I setting a resizing property wrong, or something else? Below is a before and after image of what the clipping looks like. Before Collapse: After Collapse and re-open (notice the scroll bar in the bottom view is clipped)
The problem stems from the fact that cocoa autoresizing rules work by scaling deltas from the previous state to the current state. If any of the margins go to 0 they'll never scale back up as the view grows because of the multaplicative nature of the scaling. The typical approach to working around this is to use the NSSplitView delegate methods to prevent the split view from getting to small and then have it snap shut - which internally keeps the collapsed view at the minimum size. Here's a link to the split view documentation. Also, if you think about the user experience, your views probably look really awful when they're sized down below a certain point - views probably start overlapping, and becoming too small to show their content. Adding this snapping-collapsing behavior addresses both problems. If you want to see an example of this, Mac OS X's Mail.app snaps its inline message view closed when it gets to a certain height. You should mimic that behavior.
I have the same problem. Fixed it using BWToolkit's split views, which allow you to determine the maximum and minimum height for each view.
You could "reset" things via NSUserDefaults, possibly.. There are keys for such things as NSSplitView Subview Frames, etc, to which you can assign coordinates, a la 0.000000, 0.000000, 0.000000, 720.000000, NO
While Jon Hess could describe the problem well (as soon as a subview's width becomes 0 the autosizing information gets lost for auto-width elements), the solution is not really given for all cases. Constraining the width did not help in my case, as the subview can be collapsed. I managed to achieve an acceptable solution, by implementing the splitView delegate method -splitviewWillResizeSubviews: to maintain a minimum width by setting the subview to hidden instead of shrinking it to zero: - (void)splitViewWillResizeSubviews:(NSNotification *)notification { NSUInteger divider = [[[notification userInfo] valueForKey:#"NSSplitViewDividerIndex"] intValue]; NSView *subview = nil; if(divider == SPLITVIEW_DIVIDER_SIDEBAR) { subview = (NSView*)[self.splitView.subviews objectAtIndex:SPLITVIEW_SIDEBAR_INDEX]; } if(subview) { if(subview.frame.size.width < SPLITVIEW_MINIMUM_SIDEBAR_WIDTH) { CGRect correctedFrame = subview.frame; correctedFrame.size.width = SPLITVIEW_MINIMUM_SIDEBAR_WIDTH; subview.frame = correctedFrame; subview.hidden = YES; } else { subview.hidden = NO; } } }