29

I am trying to use the new Auto Layout in Lion because it seems quite nice. But I can not find good information about how to do things. For example:

I have two labels:

+----------------+
| +------------+ |
| + label 1    | |
| +------------+ |
|                |
| +------------+ |
| | label 2    | |
| +------------+ |
+----------------+

but the first label gets not always populated with content, sometimes there ist just no content for it. What I would like to do is to automatically show the label 2 where label 1 would be when it would have content.

+----------------+
| +------------+ |
| + label 2    | |
| +------------+ |
|                |
|                |
|                |
|                |
+----------------+

What constrains would I have to add so it works automatically with autolayout? I understand that I could just code everything, but I have about 30 such labels and images and buttons of different styles and shapes which are all optional and I don't want to add hundrets of lines of code when it could work automatically quite nice too.

If it does not work then I will just use a WebView and do it with HTML and CSS.

Jeena
  • 2,172
  • 3
  • 27
  • 46
  • I’m not sure if that’s possible with auto layout but it looks like what you really want is a table. –  Oct 27 '11 at 21:56
  • A table would help if it would just be from the top to bottom, but some stuff are too from left to right and should take the place of others and stuff. But the idea is not bad I admit. – Jeena Oct 28 '11 at 06:28

8 Answers8

24

This is possible with auto layout, but doesn't exactly scale well.

So, taking your example, let's say you have label A, and label B (or button or anything else really). First start by adding a top constraint to the superview for A. Then a vertical spacing constraint between A and B. This is all normal so far. If you were to remove A at this point, B would have ambiguous layout. If you were to hide it, it would still occupy it's space including the space between the labels.

Next you need to add another constraint from B, to the top of the superview. Change the priority on this to be lower than the others (say 900) and then set it's constant to be standard (or other smaller value). Now, when A is removed from it's superview, the lower priority constraint will kick in and pull B towards the top. The constraints look something like this:

Interface Builder screenshot

The issue comes when you try to do this with a long list of labels.

Matt
  • 179
  • 7
David Beck
  • 10,099
  • 5
  • 51
  • 88
  • how does the second part of this work? If you add another constraint from B to the superview, doesn't it always trigger regardless of the priority? – kevinl Jan 08 '14 at 22:37
  • nevermind, I was trying to use this specific logic on my scenario which didn't end up working: I have Button A, B, C. I'm removing B, and I want C to take the place of B. – kevinl Jan 08 '14 at 22:45
  • This is a clean solution for the case presented. For the case of more labels, it would probably be easiest to replicate this in code. IB suggests a solution in the pop-up "Add new constraint" editor, when it references "nearest neighbor". If it actually did just that, it would work out of the box. I think the problem is that they are binding ("neigbhor") too early, and you can see this in the storyboard file format. – Chris Conover Oct 14 '14 at 19:50
10

Collapsing UILabel subclass

One simple solution is to just subclass UILabel and change the intrinsic content size.

@implementation WBSCollapsingLabel

- (CGSize)intrinsicContentSize
{
    if (self.isHidden) {
        return CGSizeMake(UIViewNoIntrinsicMetric, 0.0f);
    } else {
        return [super intrinsicContentSize];
    }
}

- (void)setHidden:(BOOL)hidden
{
    [super setHidden:hidden];

    [self updateConstraintsIfNeeded];
    [self layoutIfNeeded];
}

@end
Cameron Lowell Palmer
  • 21,528
  • 7
  • 125
  • 126
  • 1
    This seems like a really simple, elegant way to go. I'm curious why it hasn't been voted higher if it actually does work? – software evolved Jun 10 '14 at 16:57
  • Well... great question. In general auto layout topics haven't been well covered on SO. I also think many people just hook up an outlet to the constraint and whack the height. Sub-classing is also a bit annoying. – Cameron Lowell Palmer Jun 13 '14 at 11:56
  • The problem with this approach is that it does not take into account the margins you usually have around your views. – tcurdt Jul 02 '14 at 15:02
  • The margins as in H:|-(8)-[view1]-(12)-[hidden1]-(8)-|? Yeah... You're right, but that is the same problem when you had with frames. You'll still have to make sure the spacing between view is handled, but that is a separate question! – Cameron Lowell Palmer Jul 04 '14 at 15:50
  • 1
    This is such a neat solution it works in Cocoa as well, just switch `UIViewNoIntrinsicMetric` to `NSViewNoInstrinsicMetric`, `updateConstraintsIfNeeded` to `updateConstraintsForSubtreeIfNeeded` and `layoutIfNeeded` to `layoutSubtreeIfNeeded` and it works like a charm. Thank you! – bithavoc Feb 20 '15 at 05:12
4

This category makes collapsing Auto Layout constrained views really simple:

https://github.com/depth42/AutolayoutExtensions

I just added it to a project and it works great.

Christian
  • 584
  • 6
  • 6
  • I'm trying to use this in my project, but new public outlet `PWHidingMasterView` doesn't appear in Xcode for the views on my project. When I open the sample project the outlet is there. I cannot figure out what's different from my project and the sample one. Any tips? – Ricardo Sanchez-Saez Mar 13 '14 at 01:30
2

I don't think you could do it that way. If you made the layout for label 2 be based on a distance constraint from label 1, even if you made label 1 auto-collapse to zero height when it has no content, label 2 is still going to be that distance down, ie in:

+----------------+
| +------------+ |
| + label 1    | |
| +------------+ |
|        ^       |
|        ^       !
| +------------+ |
| | label 2    | |
| +------------+ |
+----------------+

Where ^ is the autolayout distance constraint - If Label 1 knows how to become zero height when it's string is empty, you're still going to get:

+----------------+
| +------------+ |
|        ^       |
|        ^       !
| +------------+ |
| | label 2    | |
| +------------+ |
+----------------+

Maybe it is possible by creating your NSLayoutConstraint manually. You could make the second attribute be the height of label 1, make the constant zero, and then carefully work out what the multiplier would be to make the distance be what you want based on a multiple of the non-zero label height.

But having done all this, you've now coded an NSLabel subclass that auto-sizes, created a constraint object manually instead of via the visual language, and bent NSLayoutConstraint beyond its will.

I think you're better off just changing the frame of label 2 if label 1's string is blank!

Mark
  • 1,304
  • 12
  • 22
  • without setting constant to 0. is that any direct option available in interface builder? –  Aug 07 '14 at 04:48
1

Here's an example of how I handled this programmatically rather than using Interface Builder. In summary; I only add the view if it's enabled and then iterate over subviews, adding vertical constraints as I go.

Note that the views in question are initialized prior to this.

/*
  Begin Auto Layout
*/
NSMutableArray *constraints = [NSMutableArray array];
NSMutableDictionary *views = [[NSMutableDictionary alloc] init];


/*
  Label One
*/
if (enableLabelOne) {
    [contentView addSubview:self.labelOne];

    self.labelOne.translatesAutoresizingMaskIntoConstraints = NO;

    [views setObject:self.labelOne
              forKey:@"_labelOne"];

    [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_labelOne(44)]"
                                                                             options:0
                                                                             metrics:nil
                                                                               views:views]];

    [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_labelOne]-|"
                                                                             options:0
                                                                             metrics:nil
                                                                               views:views]];
}

/*
    Label Two
*/
if (enableLabelTwo) {
    [contentView addSubview:self.labelTwo];

    self.labelTwo.translatesAutoresizingMaskIntoConstraints = NO;

    [views setObject:self.labelTwo
              forKey:@"_labelTwo"];

    [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_labelTwo(44)]"
                                                                             options:0
                                                                             metrics:nil
                                                                               views:views]];
}

[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_labelTwo]-|"
                                                                         options:0
                                                                         metrics:nil
                                                                           views:views]];

/*
  Dynamically add vertical spacing constraints to subviews
*/
NSArray *subviews = [contentView subviews];

if ([subviews count] > 0) {
    UIView *firstView = [subviews objectAtIndex:0];
    UIView *secondView = nil;
    UIView *lastView = [subviews lastObject];

    [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[firstView]"
                                                                             options:0
                                                                             metrics:nil
                                                                               views:NSDictionaryOfVariableBindings(firstView)]];

    for (int i = 1; i < [subviews count]; i++) {
        secondView = [subviews objectAtIndex:i];
        [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[firstView]-10-[secondView]"
                                                                                 options:0
                                                                                 metrics:nil
                                                                                   views:NSDictionaryOfVariableBindings(firstView, secondView)]];
        firstView = secondView;
    }

    [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[lastView]-|"
                                                                             options:0
                                                                             metrics:nil
                                                                               views:NSDictionaryOfVariableBindings(lastView)]];
}


[self addConstraints:constraints];

I'm only setting the lastView constraint because this code was adapted from something inside of a UIScrollView.

I originally implemented this based on this Stack Overflow answer and changed things to suit my own needs.

Community
  • 1
  • 1
Paul Young
  • 1,489
  • 1
  • 15
  • 34
0

I found a seemlingly decent way to do this as well. It's similar to David's. Here's how it works in code. I created the superview and all it's subviews, even the ones that may not always be showing. I added many of the constraints such as V:|-[_btn] to the superview. As you can see at the end of those constraints there is no link to the bottom on the superview. I then created two arrays of constraints for both states of the view, for me the difference is a 'More Options' disclosure triangle. Then when the triangle is clicked depending on it's state I add and remove constraints and subviews accordingly. For example to add I do:

[self.backgroundView removeConstraints:self.lessOptionsConstraints];
[self.backgroundView addSubview:self.nameField];
[self.backgroundView addConstraints:self.moreOptionsConstraints];

The constraints I removed tied the button to the bottom of the superview like V:[_btn]-|. The constraints I added look like V:[_btn]-[_nameField]-| as you can see this constraint places the new view in between the original view above it and the bottom of the superview which extends the superview's height.

Community
  • 1
  • 1
Keith Smiley
  • 61,481
  • 12
  • 97
  • 110
0

I've found another way to do this. This methodology can be applied anywhere, has no scaling problems; and handles the margins as well. And you don't need 3rd party things for it.

First, dont use this layout:

V:|-?-[Label1]-10-[Label2]-10-|
H:|-?-[Label1]-?-|
H:|-20-[Label2]-20-|

Use these instead:

("|" is the real (outer) container)
V:|-?-[Label1]-0-[Label2HideableMarginContainer]-0-|
H:|-?-[Label1]-?-|
H:|-0-[Label2HideableMarginContainer]-0-|

("|" is Label2HideableMarginContainer)
V:|-10-[Label2]-10-|
H:|-20-[Label2]-20-|

So what have we done now? Label2 is not directly used in the layout; it's placed into a Margin-Container. That container is used as a proxy of Label2, with 0 margins in the layout. The real margins are put inside of the Margin-Container.

Now we can hide Label2 with:

  • setting Hidden to YES on it

AND

  • Disabling the Top, Bottom, Leading and Trailing constraints. So seek them out, than set Active to NO on them. This will cause the Margin-Container to have a Frame Size of (0,0); because it does have subview(s); but there aren't any (active) layout constraints which anchors those subviews to it.

Maybe a bit complex, but you only have to develop it once. All the logic can be put into a separate place, and be reused every time you need to hide smg.

Here is C# Xamarin code how to seek those constraints which anchors the subview(s) to the inner edges of the Margin-Container view:

public List<NSLayoutConstraint> SubConstraints { get; private set; }

private void ReadSubContraints()
{
    var constraints = View.Constraints; // View: the Margin-Container NSView
    if(constraints?.Any() ?? false)
    {
        SubConstraints = constraints.Where((NSLayoutConstraint c) => {
            var predicate = 
                c.FirstAttribute == NSLayoutAttribute.Top ||
                c.FirstAttribute == NSLayoutAttribute.Bottom ||
                c.FirstAttribute == NSLayoutAttribute.Leading ||
                c.FirstAttribute == NSLayoutAttribute.Trailing;
            predicate &= ViewAndSubviews.Contains(c.FirstItem); // ViewAndSubviews: The View and View.Subviews
            predicate &= ViewAndSubviews.Contains(c.SecondItem);
            return predicate;
        }).ToList();
    }
}
nvirth
  • 1,599
  • 1
  • 13
  • 21
0

Solved this issue programmatically. I have a few buttons in a row, and I may decide to hide one at any time.

enter image description here

Used Cartography to replace them each time hidden changes in any of them.

let buttons = self.buttons!.filter { button in
    return !button.hidden
}

constrain(buttons, replace: self.constraintGroup) { buttons in
    let superview = buttons.first!.superview!

    buttons.first!.left == superview.left

    for var i = 1; i < buttons.count; i++ {
        buttons[i].left == buttons[i-1].right + 10
    }

    buttons.last!.right == superview.right
}
Vojto
  • 6,901
  • 4
  • 27
  • 33