170

I feel like it's a fairly common paradigm to show/hide UIViews, most often UILabels, depending on business logic. My question is, what is the best way using AutoLayout to respond to hidden views as if their frame was 0x0. Here is an example of a dynamic list of 1-3 features.

Dynamic features list

Right now I have a 10px top space from the button to the last label, which obviously won't slide up when the the label is hidden. As of right now I created an outlet to this constraint and modifying the constant depending on how many labels I'm displaying. This is obviously a bit hacky since I'm using negative constant values to push the button up over the hidden frames. It's also bad because it's not being constrained to actual layout elements, just sneaky static calculations based on known heights/padding of other elements, and obviously fighting against what AutoLayout was built for.

I could obviously just create new constraints depending on my dynamic labels, but that's a lot of micromanaging and a lot of verbosity for trying to just collapse some whitespace. Are there better approaches? Changing frame size 0,0 and letting AutoLayout do its thing with no manipulation of constraints? Removing views completely?

Honestly though, just modifying the constant from context of the hidden view requires a single line of code with simple calculation. Recreating new constraints with constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant: seems so heavy.

Edit Feb 2018: See Ben's answer with UIStackViews

Ryan Romanchuk
  • 10,819
  • 6
  • 37
  • 41
  • Thanks Ryan for this question . I was going nuts what to do for the cases as you have asked . Everytime i check out for the tutorial for autolayout , most of them says refer to raywenderlich tutorial site which i find a bit hard to make it out. – Nassif Feb 07 '14 at 11:56

13 Answers13

236

My personal preference for showing/hiding views is to create an IBOutlet with the appropriate width or height constraint.

I then update the constant value to 0 to hide, or whatever the value should be to show.

The big advantage of this technique is that relative constraints will be maintained. For example let's say you have view A and view B with a horizontal gap of x. When view A width constant is set to 0.f then view B will move left to fill that space.

There's no need to add or remove constraints, which is a heavyweight operation. Simply updating the constraint's constant will do the trick.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Max MacLeod
  • 26,115
  • 13
  • 104
  • 132
  • I really like this answer. So basically, if i'm understanding correctly, set fixed height or weight constraints, create an IBOutlet for those constraints, and if that view is hidden simply change to a constant of 0.f and the rest of the elements should fall into place? – Ryan Romanchuk Oct 25 '13 at 11:23
  • 1
    exactly. As long as - in this example - you position view B to the right of view A using a horizontal gap. I use this technique all the time and it works very well – Max MacLeod Oct 25 '13 at 11:47
  • @MaxMacLeod What if the label is potentially multi-line? In this case, a height constraint would not be appropriate, right? – markdorison Jan 29 '14 at 23:10
  • @markdorison Hi Mark I saw your post on twitter actually. I take it in your case the label is variable height? This should work ok but the height will of course have to fit the content using boundingRectWithSize. I wonder if you'll need to precalculate the heights for each cell. – Max MacLeod Jan 30 '14 at 08:59
  • 9
    @MaxMacLeod Just to make sure: if the gap between the two views are x, in order for the view B to start from view A position, we must also change the gap constraint constant to 0, too, right? – kernix Mar 07 '14 at 09:14
  • 1
    @kernix yes if there would need to be a horizontal gap constraint between the two views. constant 0 of course means no gap. So hiding view A by setting it's width to 0 would move view B left to be flush with the container. Btw thanks for the up votes! – Max MacLeod Mar 07 '14 at 09:34
  • 2
    @MaxMacLeod Thanks for your answer. I tried doing this with a UIView that contains other views, but that view's subviews aren't hidden when I set its height constraint to 0. Is this solution supposed to work with views containing subviews? Thanks! – shaunlim Aug 27 '14 at 23:49
  • 7
    Would also appreciate a solution that works for a UIView containing other views... – Mark Gibaud Oct 23 '14 at 13:11
  • I think that your solution would work for my problem. If so could you provide a code example since im new to IOS development and constraints really confuse me. This is the post http://stackoverflow.com/questions/27347937/changing-position-of-a-uilabel-for-a-custo @MaxMacLeod – Timo Cengiz Dec 09 '14 at 17:49
  • So question on this one, since this is truly auto layout, how would you know what to set the height to? Would you want to have the control reset it's height, and if so, how? – Rob Bonner Sep 06 '15 at 23:50
  • @RobBonner great question. A label could have dimensions set automatically with intrinsic content size. However that's not going to work in this case as you will have an explicit height constraint. So you're either going to have the height hardcoded in the Xib - then noted in the view controller - or set entirely programmatically in the view controller. – Max MacLeod Sep 07 '15 at 05:54
  • 1
    Thanks @Max MacLeod But I have a small problem. My view has vertical margins, so even if I add a zero height constraint with high or required priority, then I see an error in the console because these constraints cannot satisfied at the same time. – Ricardo Dec 10 '15 at 12:49
101

The solution of using a constant 0 when hidden and another constant if you show it again is functional, but it is unsatisfying if your content has a flexible size. You'd need to measure your flexible content and set a constant back. This feels wrong, and has issues if content changes size because of server or UI events.

I have a better solution.

The idea is to set the a 0 height rule to have high priority when we hide the element so that it takes up no autolayout space.

Here's how you do that:

1. set up a width (or height) of 0 in interface builder with a low priority.

setting width 0 rule

Interface Builder won't yell about conflicts because the priority is low. Test the height behavior by setting the priority to 999 temporarily (1000 is forbidden to mutate programmatically, so we won't use it). Interface builder will probably now yell about conflicting constraints. You can fix these by setting priorities on related objects to 900 or so.

2. Add an outlet so you can modify the priority of the width constraint in code:

outlet connection

3. Adjust the priority when you hide your element:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999
SimplGy
  • 20,079
  • 15
  • 107
  • 144
  • @SimplGy can you please tell me what is this place.closingSoon? – Niharika Feb 17 '17 at 06:09
  • It's not part of the general solution, @Niharika, It's just what the business rule was in my case (show the view if the restaurant is / is not closing soon). – SimplGy Feb 19 '17 at 00:35
88

UIStackView is probably the way to go for iOS 9+. Not only does it handle the hidden view, it will also remove additional spacing and margins if set up correctly.

Ben Packard
  • 26,102
  • 25
  • 102
  • 183
  • 8
    Watch out when using UIStackView in a UITableViewCell. For me, once the view got very complicated the scrolling started to become choppy where it would stutter every time a cell was scrolled in/out. Under the covers the StackView is adding/removing constraints and this will not necessarily be efficient enough for smooth scrolling. – Kento Feb 16 '16 at 18:31
  • 7
    Would be nice to give a small example of how stack view handles this. – lostintranslation Sep 09 '16 at 19:47
  • @Kento is this still an issue on iOS 10? – Crashalot Oct 31 '17 at 20:31
  • 3
    Five years later... should I update and set this as the accepted answer? It's been available for three release cycles now. – Ryan Romanchuk Feb 13 '18 at 06:02
  • 1
    @RyanRomanchuk You should, this is definitely the best answer now. Thanks Ben! – Duncan Lukkenaer Jul 30 '18 at 16:19
  • I am using UIStackView but I am not able to work with show/hide fields properly. Please check my question and suggest me something. Please follow this link https://stackoverflow.com/questions/51720099/uistackview-loading-views-from-xib-and-updating-height-constraint-of-subview-di – iamVishal16 Aug 08 '18 at 11:06
  • @Kento: BoxView is similar to UIStackView, it is compatible with anything and has more layout options. – Vladimir Jun 28 '20 at 12:46
12

In this case, I map the height of the Author label to an appropriate IBOutlet:

@property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;

and when I set the height of the constraint to 0.0f, we preserve the "padding", because the Play button's height allows for it.

cell.authorLabelHeight.constant = 0; //Hide 
cell.authorLabelHeight.constant = 44; //Show

enter image description here

Mitul Marsoniya
  • 5,272
  • 4
  • 33
  • 57
10

There's a lot of solutions here but my usual approach is different again :)

Set up two sets of constraints similar to Jorge Arimany's and TMin's answer:

enter image description here

All three marked constraints have a the same value for the Constant. The constraints marked A1 and A2 have their Priority set to 500, while the constraint marked B has it's Priority set to 250 (or UILayoutProperty.defaultLow in code).

Hook up constraint B to an IBOutlet. Then, when you hide the element, you just need to set the constraint priority to high (750):

constraintB.priority = .defaultHigh

But when the element is visible, set the priority back to low (250):

constraintB.priority = .defaultLow

The (admittedly minor) advantage to this approach over just changing isActive for constraint B is that you still have a working constraint if the transient element gets removed from the view by other means.

SeanR
  • 7,899
  • 6
  • 27
  • 38
5

Subclass the view and override func intrinsicContentSize() -> CGSize. Just return CGSizeZero if the view is hidden.

Daniel
  • 683
  • 1
  • 9
  • 20
  • 2
    This is great. Some UI elements need a trigger to `invalidateIntrinsicContentSize` though. You can do that in an overridden `setHidden`. – DrMickeyLauer Mar 07 '16 at 09:23
5

I just found out that to get a UILabel to not take up space, you have to hide it AND set its text to an empty string. (iOS 9)

Knowing this fact/bug could help some people simplify their layouts, possibly even that of the original question, so I figured I'd post it.

Matt Koala
  • 2,171
  • 2
  • 18
  • 14
  • 1
    I don't think that you need to hide it. Setting its text to an empty string should suffice. – Rob VS Jul 11 '16 at 12:27
4

I'm surprised that there is not a more elegant approach provided by UIKit for this desired behavior. It seems like a very common thing to want to be able to do.

Since connecting constraints to IBOutlets and setting their constants to 0 felt yucky (and caused NSLayoutConstraint warnings when your view had subviews), I decided to create an extension that gives a simple, stateful approach to hiding/showing a UIView that has Auto Layout constraints

It merely hides the view and removes exterior constraints. When you show the view again, it adds the constraints back. The only caveat is that you'll need to specify flexible failover constraints to surrounding views.

Edit This answer is targeted at iOS 8.4 and below. In iOS 9, just use the UIStackView approach.

Albert Bori
  • 9,832
  • 10
  • 51
  • 78
  • 1
    This is my preferred approach too. It's a significant improvement on other answers here as it doesn't just squash the view to zero size. The squash approach has will have knock on issues for anything but pretty trivial payouts. An example of this is the large gap where the hidden item is in [this other answer](http://stackoverflow.com/a/22386997/2547229). And you can end up with constraint violations as mentioned. – Benjohn Nov 10 '15 at 17:04
  • @DanielSchlaug Thanks for pointing that out. I've updated the license to MIT. However, I do require a mental high-five every time someone implements this solution. – Albert Bori Jan 15 '16 at 17:12
4

The best practice is, once everything has the correct layout constraints, add a height or with constraint, depending how you want the surrounding views to move and connect the constraint into an IBOutlet property.

Make sure that your properties are strong

in code yo just have to set the constant to 0 and activate it, tho hide the content, or deactivate it to show the content. This is better than messing up with the constant value an saving-restoring it. Do not forget to call layoutIfNeeded afterwards.

If the content to be hidden is grouped, the best practice is to put all into a view and add the constraints to that view

@property (strong, nonatomic) IBOutlet UIView *myContainer;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *myContainerHeight; //should be strong!!

-(void) showContainer
{
    self.myContainerHeight.active = NO;
    self.myContainer.hidden = NO;
    [self.view layoutIfNeeded];
}
-(void) hideContainer
{
    self.myContainerHeight.active = YES;
    self.myContainerHeight.constant = 0.0f;
    self.myContainer.hidden = YES;
    [self.view layoutIfNeeded];
}

Once you have your setup you can test it in IntefaceBuilder by setting your constraint to 0 and then back to the original value. Don't forget to check other constraints priorities so when hidden there is no conflict at all. other way to test it is to put it to 0 and set the priority to 0, but, you should not forget to restore it to the highest priority again.

Jorge Arimany
  • 5,814
  • 2
  • 28
  • 23
  • 1
    That's because the constraint and the view could not always going to be retained by the view hierarchy, so if you deactivate the constraint and hide the view you still retain them to show up them again regardless of what your view hierarchy decide to retain or not. – Jorge Arimany May 30 '16 at 09:06
3

I build category to update constraints easily:

[myView1 hideByHeight:YES];

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

enter image description here

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

My preferred method is very similar to that suggested by Jorge Arimany.

I prefer to create multiple constraints. First create your constraints for when the second label is visible. Create an outlet for the constraint between button and the 2nd label (if you are using objc make sure its strong). This constraint determines the height between the button and the second label when it's visible.

Then create another constraint that specifies the height between the button and the top label when the second button is hidden. Create an outlet to the second constraint and make sure this outlet has a strong pointer. Then uncheck the installed checkbox in interface builder, and make sure the first constraint's priority is lower than this second constraints priority.

Finally when you hide the second label, toggle the .isActive property of these constraints and call setNeedsDisplay()

And that's it, no Magic numbers, no math, if you have multiple constraints to turn on and off you can even use outlet collections to keep them organized by state. (AKA keep all the hidden constraints in one OutletCollection and the non-hidden ones in another and just iterate over each collection toggling their .isActive status).

I know Ryan Romanchuk said he didn't want to use multiple constraints, but I feel like this isn't micromanage-y, and is simpler that dynamically creating views and constraints programmatically (which is what I think he was wanting to avoid if I'm reading the question right).

I've created a simple example, I hope it's useful...

import UIKit

class ViewController: UIViewController {

    @IBOutlet var ToBeHiddenLabel: UILabel!


    @IBOutlet var hiddenConstraint: NSLayoutConstraint!
    @IBOutlet var notHiddenConstraint: NSLayoutConstraint!


    @IBAction func HideMiddleButton(_ sender: Any) {

        ToBeHiddenLabel.isHidden = !ToBeHiddenLabel.isHidden
        notHiddenConstraint.isActive = !notHiddenConstraint.isActive
        hiddenConstraint.isActive = !hiddenConstraint.isActive

        self.view.setNeedsDisplay()
    }
}

enter image description here

TMin
  • 2,280
  • 1
  • 25
  • 34
0

I will provide my solution too, to offer variety .) I think creating an outlet for each item's width/height plus for the spacing is just ridiculous and blows up the code, the possible errors and number of complications.

My method removes all views (in my case UIImageView instances), selects which ones need to be added back, and in a loop, it adds back each and creates new constraints. It's actually really simple, please follow through. Here's my quick and dirty code to do it:

// remove all views
[self.twitterImageView removeFromSuperview];
[self.localQuestionImageView removeFromSuperview];

// self.recipients always has to be present
NSMutableArray *items;
items = [@[self.recipients] mutableCopy];

// optionally add the twitter image
if (self.question.sharedOnTwitter.boolValue) {
    [items addObject:self.twitterImageView];
}

// optionally add the location image
if (self.question.isLocal) {
    [items addObject:self.localQuestionImageView];
}

UIView *previousItem;
UIView *currentItem;

previousItem = items[0];
[self.contentView addSubview:previousItem];

// now loop through, add the items and the constraints
for (int i = 1; i < items.count; i++) {
    previousItem = items[i - 1];
    currentItem = items[i];

    [self.contentView addSubview:currentItem];

    [currentItem mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(previousItem.mas_centerY);
        make.right.equalTo(previousItem.mas_left).offset(-5);
    }];
}


// here I just connect the left-most UILabel to the last UIView in the list, whichever that was
previousItem = items.lastObject;

[self.userName mas_remakeConstraints:^(MASConstraintMaker *make) {
    make.right.equalTo(previousItem.mas_left);
    make.leading.equalTo(self.text.mas_leading);
    make.centerY.equalTo(self.attachmentIndicator.mas_centerY);;
}];

I get clean, consistent layout and spacing. My code uses Masonry, I strongly recommend it: https://github.com/SnapKit/Masonry

Zoltán
  • 1,422
  • 17
  • 22
0

Try BoxView, it makes dynamic layout concise and readable.
In your case it is:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Vladimir
  • 7,670
  • 8
  • 28
  • 42