4
votes

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:

initial window display

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:

display after window resize

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:

Initial display, with NSScrollView constraints

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()
3
A clarification that might shed some additional light on the problem - the resize must be a horizontal resize. Vertical resizes don't re-evaluate the constraints. But if I resize vertically, then resize horizontally, everything adjusts to fill the screen.freakboy3742
A second clarification - It turns out that this code works exactly as expected under Mavericks, but causes problems under Mountain Lion.freakboy3742

3 Answers

9
votes

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.

2
votes

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:

  1. Window -- set delegate and IBOutlet property

  2. ScrollView -- fixed edges, no border, don't draw background

  3. documentView -- fixed edges 0, then another trailing and bottom, clipView ≥ 0 @499 and clipView ≤ 0 @501 for both trailing and bottom constraints to documentView

  4. label and text field in horizontal stack view, in vertical stack view

  5. vertical stack view fixed edges default, then another bottom, bottom ≤ default @499 and ≥ default @750

  6. horizontal stack view leading and trailing fixed 0

  7. label and text field align Y center to horizontal stack view

  8. text field top and bottom and trailing 2 @750, width ≥ 100, height ≥ 22

  9. subsequent horizontal stack views leading and trailing fixed, align text field leadings

1
votes

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.