2

I'm trying to set the layout of a view using NSLayoutConstraints.

I can set things up within the view correctly, but I also need to be able to resize the current view (the one that I'm setting the constraints for) if the constraints require it. In other words, I want to set a fixed content size with a flexible window, rather than the reverse.

So basically, I have:

NSDictionary *views = [NSDictionary dictionaryWithObjectsAndKeys:self.v1, @"v1",
                       self.v2, @"v2",
                       nil];
// Set horizontal constraints
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[v1]-|"
                                                             options:0
                                                             metrics:nil
                                                               views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[v2]-|"
                                                             options:0
                                                             metrics:nil
                                                               views:views]];
// Set vertical constraints
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[v1]-[v2]-|"
                                                             options:0
                                                             metrics:nil
                                                               views:views]];
// Initialise height constraint for v1
self.v1Height = [NSLayoutConstraint constraintWithItem:self.v1
                                                attribute:NSLayoutAttributeHeight
                                                relatedBy:NSLayoutRelationEqual
                                                   toItem:nil
                                                attribute:NSLayoutAttributeNotAnAttribute
                                               multiplier:1
                                                 constant:50];

If I later change self.v1Height (by removing it, recreating it, and then readding it), I would like the frame of self to expand (or contract) to accommodate the changed height.

I'm not sure if it's relevant (as I'm more familiar with iOS than OS X), but this is the content view of a NSPopover.

What constraint do I need to add to achieve this?

sapi
  • 9,944
  • 8
  • 41
  • 71

4 Answers4

7

There are a couple of issues:

  1. If you want your constraints to define the size of their containing view, they can't be ambiguous, as they are now. They have to fully define the space they need. For example:

    NSDictionary *views = @{@"v1" : self.v1,
                            @"v2" : self.v2};
    
    // Set horizontal constraints
    
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[v1(200)]-|"  options:0 metrics:nil views:views]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[v2(==v1)]-|" options:0 metrics:nil views:views]];
    
    // Set vertical constraints
    
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[v1]-[v2(600)]-|" options:0 metrics:nil views:views]];
    
    // Initialise height constraint for v1
    
    self.v1Height = [NSLayoutConstraint constraintWithItem:self.v1
                                                 attribute:NSLayoutAttributeHeight
                                                 relatedBy:NSLayoutRelationEqual
                                                    toItem:nil
                                                 attribute:NSLayoutAttributeNotAnAttribute
                                                multiplier:1
                                                  constant:50];
    [self addConstraint:self.v1Height];
    

    Note, I'm setting the width of v1, I'm setting v2 to be the same width as v1, and I'm setting the height of v2, also. That, combined with your subsequent creation of the v1Height constraint, now makes the view's layout unambiguous.

  2. Having done that, you also need to tell your view controller to inform the popover controller of its content size when the autolayout has been applied. Thus, your view controller could implement a viewDidLayoutSubviews:

    - (void)viewDidLayoutSubviews
    {
        [super viewDidLayoutSubviews];
    
        self.view.frame = CGRectMake(0.0, 0.0, self.view.frame.size.width, self.view.frame.size.height);
        self.contentSizeForViewInPopover = self.view.frame.size;
    }
    

    Note, that setting of the frame is very curious, but if you don't reset the origin to {0.0, 0.0}, your controls won't appear in the right place. It doesn't seem right that you have to do that, but in my experience, you do.

  3. Somewhat unrelated to your problem, you mention that you're removing v1Height "by removing it, recreating it, and then readding it". That's unnecessary and not recommended. The constant property is modifiable. As the docs say

    "Unlike the other properties, the constant may be modified after constraint creation. Setting the constant on an existing constraint performs much better than removing the constraint and adding a new one that's just like the old but for having a new constant."

    Thus, that means that you can do something as simple as:

    self.v1Height.constant = newHeight;
    [UIView animateWithDuration:0.5
                     animations:^{
                         [self layoutIfNeeded];
                     }];
    
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Hey Rob - thanks for the response; everything you've said is spot on (especially the advantage of modifying `.constant`). However, as it turns out, my main mistake was much more fundamental than that (see my answer) – sapi Jun 16 '13 at 22:50
  • @sapi Hey, if that worked for you, that's great. I may have misunderstood the question. My answer above outlines how you get a popover's frame to change on the basis of the fully qualified constraints of the views within that popover. I'm now inferring from your solution that this is not what you were trying to do, but rather just trying to solve the much simpler problem of trying to ensure that the subviews, `v1` and `v2` to adjust to fit the frame of your popover, for which you answer is a great solution. Well done and congratulations. – Rob Jun 17 '13 at 00:43
  • it was actually a combination of both things. I had initially tried something similar to your solution, but found that the popover would not shrink to fit its contents (although it would grow). That turned out to be because I set `translatesAutoresizingMaskIntoConstraints` to `NO`, as I normally do for autolayout – sapi Jun 17 '13 at 03:08
4

As it turns out, NSPopoverController is an exception to the rule, and translatesAutoresizingMaskIntoConstraints must be YES for autolayout to work correctly.

sapi
  • 9,944
  • 8
  • 41
  • 71
2

Better later than never...

For Storyboard, IB and Swift:

Just control-drag your constraint like any other control to your viewController

@IBOutlet weak var spaceToTopThatIDecided: NSLayoutConstraint!

then change it simply:

spaceToTopThatIDecided.constant = 44

That's all!

Luismi
  • 1,071
  • 1
  • 13
  • 18
0

I found a easy solution to update constraints (resize ) with a category:

Hide autolayout UIView : How to get existing NSLayoutConstraint to update this one

Community
  • 1
  • 1
Damien Romito
  • 9,801
  • 13
  • 66
  • 84