AutoLayout with hidden UIViews?

IosObjective CAutolayout

Ios Problem Overview


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

Ios Solutions


Solution 1 - Ios

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.

Solution 2 - Ios

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

Solution 3 - Ios

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.

Solution 4 - Ios

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

Solution 5 - Ios

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.

Solution 6 - Ios

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

Solution 7 - Ios

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.

Solution 8 - Ios

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.

Solution 9 - Ios

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.

Solution 10 - Ios

I build category to update constraints easily:

[myView1 hideByHeight:YES];

Answer here: https://stackoverflow.com/questions/22384927/hide-autolayout-uiview-how-to-get-existing-nslayoutconstraint-to-update-this-o/

enter image description here

Solution 11 - Ios

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

Solution 12 - Ios

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

Solution 13 - Ios

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
    ]

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRyan RomanchukView Question on Stackoverflow
Solution 1 - IosMax MacLeodView Answer on Stackoverflow
Solution 2 - IosSimplGyView Answer on Stackoverflow
Solution 3 - IosBen PackardView Answer on Stackoverflow
Solution 4 - IosMitul MarsoniyaView Answer on Stackoverflow
Solution 5 - IosSeanRView Answer on Stackoverflow
Solution 6 - IosDanielView Answer on Stackoverflow
Solution 7 - IosMatt KoalaView Answer on Stackoverflow
Solution 8 - IosAlbert BoriView Answer on Stackoverflow
Solution 9 - IosJorge ArimanyView Answer on Stackoverflow
Solution 10 - IosDamien RomitoView Answer on Stackoverflow
Solution 11 - IosTMinView Answer on Stackoverflow
Solution 12 - IosZoltánView Answer on Stackoverflow
Solution 13 - IosVladimirView Answer on Stackoverflow