我缺少什么阻止我使用自动布局制作类似 NSStackView 的通用容器?

What am I missing that prevents me from making a generic NSStackView-like container with Auto Layout?

我正在尝试使用自动布局 for OS X 实现容器视图,其操作类似于 NSStackView,但有一些 NSStackView 无法处理的差异(无论如何我都需要 10.7 的支持)。我的规则是:

我认为这可以通过一种简单的方式完成,使用 |[view]| 视觉格式依次连接主要方向上的视图,然后连接次要方向上的视图。缺少可伸缩视图将使用内部内容大小为 0x0 的 NSView 作为最后一个视图来处理。

这基本上奏效了。不幸的是,形式的水平方向堆栈的嵌套树出现了歧义(出于说明目的使用 HTML 表示)

.box {
  display: inline-block;
  border: 1px solid black;
  padding: 0.5em 0.5em 0.5em 0.5em;
}

.outermost-box {
  display: inline-block;
  border: 1px solid black;
  padding: 0.5em 0.5em 0.5em 0.5em;
  width: 100%;
}
<div class="outermost-box">
  <div class="box">
    <input type="button" value="These">
    <input type="button" value="Buttons" disabled>
  </div>
  <div class="box">
    <input type="button" value="are">
    <div class="box">
      <input type="button" value="in" disabled>
    </div>
  </div>
  <div class="box">
    <div class="box">
      <input type="button" value="nested">
      <div class="box">
        <input type="button" value="boxes" disabled>
      </div>
    </div>
  </div>
</div>

其中所有堆栈都没有弹性子视图。根据我的定义,最外面的盒子会拉伸,而且只有那个盒子会拉伸。但是,Auto Layout 会将额外的 space 随机分配给其中一个内部框:

将额外视图更改为 NSLayoutRelationLessThanOrEqual 关系 (last view.trailing <= superview.trailing) 也无济于事。不过,我将在接下来的 post 中保留此模型,因为我的下一次尝试都基于它。

然后我决定尝试让容器询问它的超级视图是否应该展开。这解决了上述问题,但引入了另一个问题,即在水平和垂直之间交替的深层容器链:

标有 "Right Margin Test" 的按钮应该 伸展,但它们要么没有伸展,要么在伸展但同时剪裁了侧面的视图(我没有现在的截图;抱歉)。

然后我决定在右边缘同时启用 <= 和备用 == 约束,如果应该有额外的,则将 == 设置为低优先级space。这个新的大部分工作,但现在有一个奇怪的问题。如果我将上面显示的第 3 页上的 window 调整得足够大,然后切换到第 4 页,我得到

然后如果我调整大小我得到

尽管第 4 页在任何情况下都应在底部显示 space。有时可以看到按钮的底部,并且可视化它的垂直约束表明它认为它想要与单选按钮矩阵一样高(现在是一个 NSMatrix;将它更改为一堆 NSButtons 将等到我修复所有这些自动布局问题)。

我真的不确定发生了什么或如何解决这些问题。我尝试使我提到的 == 约束具有自己的可设置 "real hugging priority",但这只会让事情以更壮观的方式破裂。'

选项卡视图的位置最初太低并且需要几个布局周期才能正确设置也存在问题...

显示的一切都是用这些容器完成的,NSBoxes 有一个子视图,NSTabs 有一个子视图。我将在下面粘贴我的容器的代码。

那么自动布局又如何呢?我不明白我不能只用明显的代码让它正常工作吗?或者 NSStackView 可以做我想做的所有事情而我应该只使用它吗? (假设 alignmentWidthHeight 视为有效,Interface Builder 似乎没有这么说)。

谢谢!

// 15 august 2015
#import "uipriv_darwin.h"

// TODOs:
// - tab on page 2 is glitched initially and doesn't grow
// - page 3 doesn't work right; probably due to our shouldExpand logic being applied incorrectly

// TODOs to confirm
// - 10.8: if we switch to page 4, then switch back to page 1, check Spaced, and go back to page 4, some controls (progress bar, popup button) are clipped on the sides

@interface boxChild : NSObject
@property uiControl *c;
@property BOOL stretchy;
@property NSLayoutPriority oldHorzHuggingPri;
@property NSLayoutPriority oldVertHuggingPri;
- (NSView *)view;
@end

@interface boxView : NSView {
    uiBox *b;
    NSMutableArray *children;
    BOOL vertical;
    int padded;

    NSLayoutConstraint *first;
    NSMutableArray *inBetweens;
    NSLayoutConstraint *last, *last2;
    NSMutableArray *otherConstraints;

    NSLayoutAttribute primaryStart;
    NSLayoutAttribute primaryEnd;
    NSLayoutAttribute secondaryStart;
    NSLayoutAttribute secondaryEnd;
    NSLayoutAttribute primarySize;
    NSLayoutConstraintOrientation primaryOrientation;
    NSLayoutConstraintOrientation secondaryOrientation;
}
- (id)initWithVertical:(BOOL)vert b:(uiBox *)bb;
- (void)onDestroy;
- (void)removeOurConstraints;
- (void)forAll:(void (^)(uintmax_t i, boxChild *b))closure;
- (boxChild *)child:(uintmax_t)i;
- (BOOL)isVertical;
- (void)append:(uiControl *)c stretchy:(int)stretchy;
- (void)delete:(uintmax_t)n;
- (int)isPadded;
- (void)setPadded:(int)p;
@end

struct uiBox {
    uiDarwinControl c;
    boxView *view;
};

@implementation boxChild

- (NSView *)view
{
    return (NSView *) uiControlHandle(self.c);
}

@end

@implementation boxView

- (id)initWithVertical:(BOOL)vert b:(uiBox *)bb
{
    self = [super initWithFrame:NSZeroRect];
    if (self != nil) {
        // the weird names vert and bb are to shut the compiler up about shadowing because implicit this/self is stupid
        self->b = bb;
        self->vertical = vert;
        self->children = [NSMutableArray new];
        self->inBetweens = [NSMutableArray new];
        self->otherConstraints = [NSMutableArray new];

        if (self->vertical) {
            self->primaryStart = NSLayoutAttributeTop;
            self->primaryEnd = NSLayoutAttributeBottom;
            self->secondaryStart = NSLayoutAttributeLeading;
            self->secondaryEnd = NSLayoutAttributeTrailing;
            self->primarySize = NSLayoutAttributeHeight;
            self->primaryOrientation = NSLayoutConstraintOrientationVertical;
            self->secondaryOrientation = NSLayoutConstraintOrientationHorizontal;
        } else {
            self->primaryStart = NSLayoutAttributeLeading;
            self->primaryEnd = NSLayoutAttributeTrailing;
            self->secondaryStart = NSLayoutAttributeTop;
            self->secondaryEnd = NSLayoutAttributeBottom;
            self->primarySize = NSLayoutAttributeWidth;
            self->primaryOrientation = NSLayoutConstraintOrientationHorizontal;
            self->secondaryOrientation = NSLayoutConstraintOrientationVertical;
        }
    }
    return self;
}

- (void)onDestroy
{
    boxChild *bc;
    uintmax_t i, n;

    [self removeOurConstraints];
    [self->first release];
    [self->inBetweens release];
    [self->last release];
    [self->last2 release];
    [self->otherConstraints release];

    n = [self->children count];
    for (i = 0; i < n; i++) {
        bc = [self child:i];
        uiControlSetParent(bc.c, NULL);
        uiDarwinControlSetSuperview(uiDarwinControl(bc.c), nil);
        uiControlDestroy(bc.c);
    }
    [self->children release];
}

- (void)removeOurConstraints
{
    [self removeConstraint:self->first];
    [self removeConstraints:self->inBetweens];
    [self removeConstraint:self->last];
    [self removeConstraint:self->last2];
    [self removeConstraints:self->otherConstraints];
}

- (void)forAll:(void (^)(uintmax_t i, boxChild *b))closure
{
    uintmax_t i, n;

    n = [self->children count];
    for (i = 0; i < n; i++)
        closure(i, [self child:i]);
}

- (boxChild *)child:(uintmax_t)i
{
    return (boxChild *) [self->children objectAtIndex:i];
}

- (BOOL)isVertical
{
    return self->vertical;
}

// TODO something about spinbox hugging
- (void)updateConstraints
{
    uintmax_t i, n;
    BOOL hasStretchy;
    NSView *firstStretchy = nil;
    CGFloat padding;
    NSView *prev, *next;
    NSLayoutConstraint *c;
    NSLayoutPriority priority;

    [super updateConstraints];
    [self removeOurConstraints];

    n = [self->children count];
    if (n == 0)
        return;
    padding = 0;
    if (self->padded)
        padding = 8.0;      // TODO named constant

    // first, attach the first view to the leading
    prev = [[self child:0] view];
    self->first = mkConstraint(prev, self->primaryStart,
        NSLayoutRelationEqual,
        self, self->primaryStart,
        1, 0,
        @"uiBox first primary constraint");
    [self addConstraint:self->first];
    [self->first retain];

    // next, assemble the views in the primary direction
    // they all go in a straight line
    // also figure out whether we have stretchy controls, and which is the first
    if ([self child:0].stretchy) {
        hasStretchy = YES;
        firstStretchy = prev;
    } else
        hasStretchy = NO;
    for (i = 1; i < n; i++) {
        next = [[self child:i] view];
        if (!hasStretchy && [self child:i].stretchy) {
            hasStretchy = YES;
            firstStretchy = next;
        }
        c = mkConstraint(next, self->primaryStart,
            NSLayoutRelationEqual,
            prev, self->primaryEnd,
            1, padding,
            @"uiBox later primary constraint");
        [self addConstraint:c];
        [self->inBetweens addObject:c];
        prev = next;
    }

    // and finally end the primary direction
    self->last = mkConstraint(prev, self->primaryEnd,
        NSLayoutRelationLessThanOrEqual,
        self, self->primaryEnd,
        1, 0,
        @"uiBox last primary constraint");
    [self addConstraint:self->last];
    [self->last retain];

    // if there is a stretchy control, add the no-stretchy view
    self->last2 = mkConstraint(prev, self->primaryEnd,
        NSLayoutRelationEqual,
        self, self->primaryEnd,
        1, 0,
        @"uiBox last2 primary constraint");
    priority = NSLayoutPriorityRequired;
    if (!hasStretchy) {
        BOOL shouldExpand = NO;
        uiControl *parent;

        parent = uiControlParent(uiControl(self->b));
        if (parent != nil)
            if (self->vertical)
                shouldExpand = uiDarwinControlChildrenShouldAllowSpaceAtBottom(uiDarwinControl(parent));
            else
                shouldExpand = uiDarwinControlChildrenShouldAllowSpaceAtTrailingEdge(uiDarwinControl(parent));
        if (shouldExpand)
            priority = NSLayoutPriorityDefaultLow;
    }
    [self->last2 setPriority:priority];
    [self addConstraint:self->last2];
    [self->last2 retain];

    // next: assemble the views in the secondary direction
    // each of them will span the secondary direction
    for (i = 0; i < n; i++) {
        prev = [[self child:i] view];
        c = mkConstraint(prev, self->secondaryStart,
            NSLayoutRelationEqual,
            self, self->secondaryStart,
            1, 0,
            @"uiBox start secondary constraint");
        [self addConstraint:c];
        [self->otherConstraints addObject:c];
        c = mkConstraint(prev, self->secondaryEnd,
            NSLayoutRelationEqual,
            self, self->secondaryEnd,
            1, 0,
            @"uiBox end secondary constraint");
        [self addConstraint:c];
        [self->otherConstraints addObject:c];
    }

    // finally, set sizes for stretchy controls
    if (hasStretchy)
        for (i = 0; i < n; i++) {
            if (![self child:i].stretchy)
                continue;
            prev = [[self child:i] view];
            if (prev == firstStretchy)
                continue;
            c = mkConstraint(prev, self->primarySize,
                NSLayoutRelationEqual,
                firstStretchy, self->primarySize,
                1, 0,
                @"uiBox stretchy sizing");
            [self addConstraint:c];
            [self->otherConstraints addObject:c];
        }
}

- (void)append:(uiControl *)c stretchy:(int)stretchy
{
    boxChild *bc;
    NSView *childView;

    bc = [boxChild new];
    bc.c = c;
    bc.stretchy = stretchy;
    childView = [bc view];
    bc.oldHorzHuggingPri = horzHuggingPri(childView);
    bc.oldVertHuggingPri = vertHuggingPri(childView);

    uiControlSetParent(bc.c, uiControl(self->b));
    uiDarwinControlSetSuperview(uiDarwinControl(bc.c), self);
    uiDarwinControlSyncEnableState(uiDarwinControl(bc.c), uiControlEnabledToUser(uiControl(self->b)));

    // if a control is stretchy, it should not hug in the primary direction
    // otherwise, it should *forcibly* hug
    if (stretchy)
        setHuggingPri(childView, NSLayoutPriorityDefaultLow, self->primaryOrientation);
    else
        // TODO will default high work?
        setHuggingPri(childView, NSLayoutPriorityRequired, self->primaryOrientation);
    // make sure controls don't hug their secondary direction so they fill the width of the view
    setHuggingPri(childView, NSLayoutPriorityDefaultLow, self->secondaryOrientation);

    [self->children addObject:bc];
    [bc release];       // we don't need the initial reference now

    [self setNeedsUpdateConstraints:YES];
}

- (void)delete:(uintmax_t)n
{
    boxChild *bc;
    NSView *removedView;

    bc = [self child:n];
    removedView = [bc view];

    uiControlSetParent(bc.c, NULL);
    uiDarwinControlSetSuperview(uiDarwinControl(bc.c), nil);

    setHorzHuggingPri(removedView, bc.oldHorzHuggingPri);
    setVertHuggingPri(removedView, bc.oldVertHuggingPri);

    [self->children removeObjectAtIndex:n];

    [self setNeedsUpdateConstraints:YES];
}

- (int)isPadded
{
    return self->padded;
}

- (void)setPadded:(int)p
{
    CGFloat padding;
    uintmax_t i, n;
    NSLayoutConstraint *c;

    self->padded = p;

    // TODO split into method (using above code)
    padding = 0;
    if (self->padded)
        padding = 8.0;
    n = [self->inBetweens count];
    for (i = 0; i < n; i++) {
        c = (NSLayoutConstraint *) [self->inBetweens objectAtIndex:i];
        [c setConstant:padding];
    }
    // TODO call anything?
}

@end

static void uiBoxDestroy(uiControl *c)
{
    uiBox *b = uiBox(c);

    [b->view onDestroy];
    [b->view release];
    uiFreeControl(uiControl(b));
}

uiDarwinControlDefaultHandle(uiBox, view)
uiDarwinControlDefaultParent(uiBox, view)
uiDarwinControlDefaultSetParent(uiBox, view)
uiDarwinControlDefaultToplevel(uiBox, view)
uiDarwinControlDefaultVisible(uiBox, view)
uiDarwinControlDefaultShow(uiBox, view)
uiDarwinControlDefaultHide(uiBox, view)
uiDarwinControlDefaultEnabled(uiBox, view)
uiDarwinControlDefaultEnable(uiBox, view)
uiDarwinControlDefaultDisable(uiBox, view)

static void uiBoxSyncEnableState(uiDarwinControl *c, int enabled)
{
    uiBox *b = uiBox(c);

    if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(b), enabled))
        return;
    [b->view forAll:^(uintmax_t i, boxChild *bc) {
        uiDarwinControlSyncEnableState(uiDarwinControl(bc.c), enabled);
    }];
}

uiDarwinControlDefaultSetSuperview(uiBox, view)

static BOOL uiBoxChildrenShouldAllowSpaceAtTrailingEdge(uiDarwinControl *c)
{
    uiBox *b = uiBox(c);

    // return NO if this box is horizontal so nested horizontal boxes don't lead to ambiguity
    return [b->view isVertical];
}

static BOOL uiBoxChildrenShouldAllowSpaceAtBottom(uiDarwinControl *c)
{
    uiBox *b = uiBox(c);

    // return NO if this box is vertical so nested vertical boxes don't lead to ambiguity
    return ![b->view isVertical];
}

void uiBoxAppend(uiBox *b, uiControl *c, int stretchy)
{
    [b->view append:c stretchy:stretchy];
}

void uiBoxDelete(uiBox *b, uintmax_t n)
{
    [b->view delete:n];
}

int uiBoxPadded(uiBox *b)
{
    return [b->view isPadded];
}

void uiBoxSetPadded(uiBox *b, int padded)
{
    [b->view setPadded:padded];
}

static uiBox *finishNewBox(BOOL vertical)
{
    uiBox *b;

    uiDarwinNewControl(uiBox, b);

    b->view = [[boxView alloc] initWithVertical:vertical b:b];

    return b;
}

uiBox *uiNewHorizontalBox(void)
{
    return finishNewBox(NO);
}

uiBox *uiNewVerticalBox(void)
{
    return finishNewBox(YES);
}

我自己解决了这个问题。

主要的变化是不再有 has-extra-space-after-it 不是通过对自身的约束来实现的,而是通过其父视图来实现的。 superview 询问 self 是否应该使用额外的 space,如果是,它分配额外的 space(使用 >= 约束到 superview 边缘而不是 == 约束)。

各种其他小修复修复了边缘情况。特别是我去的所有地方

relation = NSLayoutRelationSomething;
if (condition)
    relation = NSLayoutRelationSomethingElse;
constraint = [NSLayoutConstraint constraintWithArg:arg arg:arg
    relation:relation
    ...]

我改为使用两个约束,根据条件设置它们的优先级。 这应该是自动布局的最佳实践,因为它运行良好...

谢谢!