Hiding a UIView using AutoLayout constraints - ios

From time to time I have a subview that I would like to remove from a layout. Not only should it be hidden, but it should not be considered part of the view's 'flow', so to speak. An example:
I am looking for a strategy to hide the orange view programmatically. The layout of the boxes, and their content, is via autolayout. Two things to note:
the orange box is defining its vertical height based on the content, plus some top/bottom offsets for margins. So, setting the labels' text to nil will only 'shrink' the view down to it's internal margins, it won't have a height of 0.
Similarly, the vertical spacing between the three boxes mean that even if the orange box's height is 0, the gap between red and yellow will be twice as large as required.
A possible solution
My best suggestion is to add a constraint to the orange box, setting it's height to 0. For this to work, I need to use non-required priorities for all of the vertical constraints inside the orange box. At the same time, the container should update the constant for the constraint that separates the boxes. I don't like this approach so much since the orange box class is defining it's internal constraints with it's superview's behavior in mind. Perhaps I could live with it if the orange box view instead exposes a 'collapse' method that adds the 0 height constraint itself.
Is there a better approach?

You can do this by adding an extra constraint between the yellow and red views of a lower priority, and adjusting the priorities in code.
The short dashed constraint (orangeToRedCon is the outlet) has a priority of 999 (you can't change a required priority to a non-required, so that's why it's not 1000). The long dashed constraint (yellowToRedCon) has a priority of 500 and a constant of 20. In code, you can hide the orange view, and swap those priority levels, and that will cause the yellow view to move up to whatever value you've set for the constant value of yellowToRedCon.
-(void)changePriorities {
self.yellowToRedCon.priority = 999;
self.orangeToRedCon.priority = 500;
[UIView animateWithDuration:.5 animations:^{
self.orangeView.alpha = 0;
[self.view layoutIfNeeded];
}];
}
This method doesn't require any changes in the orange view's height.

In iOS 9 you can use UIStackView for this.
There also are polyfills for older versions: TZStackView and OAStackView

What you could do is have the height constraint of the orange view as an outlet (to be able to access it).
then animate the collapse like so:
[UIView animateWithDuration:0.3 animations:^{
orangeHeightConstraint.constant = 0;
[self.view layoutIfNeeded]
}];
The orange view will have to have a top constraint to the red view and a bottom constraint to the yellow view.
Also make sure to check Clip Subviews in IB or [orangeView clipsToBounds] programatically

I would solve this by including all "necessary" spaces of a subview as part of the subview itself. This way,
1. Red View Height = visible red part + bottom space
2. Orange View Height = visible orange part + bottom space
3. Yellow View Height = visible yellow + bottom space
When you set the Orange View Height to 0 by Autolayout, it will automatically shrink the bottom space to 0 as well.

Related

Making one UIView track the animation of a single constraint on another

I'm trying to learn how to animate using AutoLayout constraints.
I've got two UIViews, as below:
When I tap the "Up" button, I want the result to look like this. I want the red rectangle to stay the same size, but to stay pinned to the top of the gray rectangle and go along for the ride, like this:
Instead, I get this:
Here's my code:
- (IBAction)upButton:(UIButton *)sender
{
self.heightConstraint.constant = 20;
[UIView animateWithDuration:1.0
animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
- (IBAction)downButton:(UIButton *)sender
{
self.heightConstraint.constant = 438;
[UIView animateWithDuration:1.0
animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
The constraint situation looks like this (screenshot--I don't know how to copy the actual list from the Document Outline). The relevant animated constraint (self.heightConstraint) is highlighted:
I confess that I find Auto Layout Constraints sort of like playing Whack-A-Mole. Can someone please help me understand how I should go about this?
When setting constraints you need to think about how the various items relate to each other and keep in mind that the layout engine needs to be able to compute a top,left and width/height for each item. Too many constraints can be just as problematic as insufficient constraints.
In your case you want the size of the red box to be fixed, so I would set constraints on it for width and height. You also want its position relative to the left edge of the screen to be fixed, so set a leading space constraint to the superview. You want its space to the grey box to be fixed, so set a trailing space constraint to the grey box. Finally you want the top of the red box to be the same as the grey box, so select the orange box in IB, Control-drag to the grey box and select "Top" from the pop up.
For the grey box, set constraints for trailing space to the superview and bottom space to the superview (it will already have a leading space constraint to the red box). Finally, either set a constraint for height or a constraint for top space to superview. This is the constraint you will animate, so create an IBOutlet for it and change it in your code as required.
I'd suggest fixing the height of the orange box. If you have a bottom constraint for the orange box remove it.
These are the constraints I would use on the orange box:
Top to the superview / Leading to the superview / Trailing to the grey box / Height fixed / Width fixed
These are the constraints I would use on the grey box:
Top to the superview / Leading to the orange box / Bottom to the superview / Width fixed
Notice the two constraints that I bolded essentially control each view's height.
I'd attach an IBOutlet to the top constraint of each view so that I could modify these values to match the behavior you want.

Autolayout height changes when I push a new view controller

Edit: I fixed my problem, but I would be interested in understanding why my fix works. See below.
I use autolayout to build my UITableViewCells.
The cell is pretty simple with 2 labels (purple and yellow) and a textfield (green).
This works fine when displaying the first time. But when I push a new view controller, the view instantly rearranges itself. The purple label gets bigger for whatever reason.
Here is an example where I click the "Parent Account ID" cell to push a new view controller. Notice that as soon as the transition begins, the layout changes. When I come back it is still changed.
The items are created with [UILabel new] with no frame.
[self.contentView addSubview:self.textLabel];
[self.contentView addSubview:self.errorLabel];
[self.contentView addSubview:self.textField];
Then the autolayout is created using Masonry.
UIEdgeInsets insets = UIEdgeInsetsMake(3, 15, 3, 15);
[self.textLabel makeConstraints:^(MASConstraintMaker *make) {
make.left.top.equalTo(self.contentView).insets(insets);
make.width.priorityLow();
}];
[self.errorLabel makeConstraints:^(MASConstraintMaker *make) {
make.top.right.equalTo(self.contentView).insets(insets);
make.left.equalTo(self.textLabel.right).offset(5);
}];
[self.textField makeConstraints:^(MASConstraintMaker *make) {
make.left.bottom.right.equalTo(self.contentView).insets(insets);
make.top.equalTo(self.textLabel.bottom).insets(insets);
}];
Notice that I never specify a height because I don't really care about the height. (as long as it is consistent!)
Any idea why this could change? Thanks
Edit: I found a fix.
I now also set the autolayout properties of contentView.
[self.contentView makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
I am still interested in understanding why the contentView changes its size!
Your fix works because it enforces a missing constraint.
First, let's start by writing down the constraints that you created using Masonry:
The purple view must be aligned left.
The purple view must be aligned top.
The purple view's width can vary but it's min and max aren't that important.
The yellow view must be aligned right.
The yellow view must be aligned top.
The yellow view's left edge must be 5px away from the purple view's right edge.
The green view must be aligned left.
The green view must be aligned bottom.
The green view must be aligned right.
The green view's top edge must be 0px away from the purple view's bottom edge.
This might seem complete but it allows autolayout to draw your views in many different ways.
Consider the following:
According to the rules you have created above, all three possibilities are true.
As you tap on a cell, the layout is invalidated and redrawn. Given that your cell can be drawn a few different ways, it will simply draw itself and satisfy all of your constraints... and it does.
I'll be honest, I'm not 100% sure what make.edges.equalTo(self); does exactly since it is a bit ambiguous. However, one thing is for sure is that it adds the missing constraints you were looking for.
Possible missing constraints are:
The purple view's height must be equal to the green view's height.
The yellow view's height must be equal to the purple view's height.
The green view's height must be equal to the yellow view's height.
Hope this clarifies it for you.

No fixed height in interface builder, constraint predicament

The screen I'm trying to create is very simple. I have two UIViews stacked atop one another. The top UIView, topView, has height of 40px, and the UIView below, botView, takes up the rest of the screen. However, sometimes topView should not appear and I want botView to take up the entirety of the screen.
What I've tried is setting width, leading/trailing space on both views. Then topView gets distance to top layout guide, botView gets distance to bottom layout guide, and then an additional constraint for vertical spacing between topView and botView. But this results in xcode yelling at me that I haven't set a height or y constraint on one of the views. Of course, I can't set a height constraint because I do not want either of them to be fixed height.
I'm sure there must be an elegant constraint solution here that I just can't seem to figure out. Thoughts?
If topView should always be 40px height, then add a constraint for the height of topView.
As for botView, you can keep the current constraint that says its top has to be correlated with the bottom of topView. But add another constraint for bottom view with a priority lower than required that says it should be 0px away from the top of its superview. This way, when you remove topView from the containing view, botView's constraint related to topView will be deleted, and therefore will satisfy the lower priority constraint.
And if you want to push back topView in, recreate the botView to topView distant constraint with required priority.
There are a couple of ways to do this. One option
In IB lay out your views. Then pin top view 0 px from the top, bottom, left, and right. Specify a height constraint of 40 px as well. To easily do this click on your view, then in the lower right hand corner click on the button that looks like | + |. Click on the dotted red lines to specify the constraints. See image:
Do the same for the other view. Don't specify a height for this view because it's height is dependent on the 40 px view for its height. Next find the height constraint for the 40 px view. You can can use the assistant editor to control drag an outlet to you view controller just like you would with a UIButton or any other IB element. When you want to hide your 40 px view set the variables constant property to 0. When you want to show it again set it back to 40.
self.topViewHeightConstraint.constant = 0;
The other option you could go with is to pin the top view 0 px to the top, left and right. Don't provide a bottom constraint but specify the height as 40 px. For the bottom view add add a left, right, and bottom constraint of 0 px. For the top constraint add a constraint of 40 px from the top superview. Again add an outlet for the most recent top constraint and when you want the view to be larger set that constraint to be 0.
An alternative: In IB, add a distance to top layout guide constraint for BOTVIEW (it will be 40...) Then click-drag this constant to the VC to create a NSLayoutConstraint property.see also here
Now, upon the condition that hides topView, you set self.myConstraint.constant = 0; This will force botView to resize to meet the condition of being 0 from the top.
You may have to clear your current constraints to get rid of errors, but this should give the desired behavior.
As for "topView" like Tom Ahh suggests, go ahead and give it a height constraint of 40 and pin it to the top as you're already doing.

Increase View Height In Scrollview Using Auto Layout

All the views here (except the nav bar) are in a scroll view. All the scrollview's children have pinned heights and vertical spacing set between them. The top label (Thanks for using...) and bottom button (Toggle) are vertically pinned to the scrollview at the top and bottom respectively. The bottom button is also pinned to the bottom layout guide.
I want a flexible height on the red view. The red view is the only one with an inequality constraint. Height >= 64
The flexible height is working in that the height of the red view automatically expands to 152 to fill the extra space on 4'' devices.
However, I want to expand the height even more. In code, I want to expand the height of that red view to, let's say, 300 when someone taps the Toggle button.
- (IBAction)toggle:(id)sender
{
[self.scrollView layoutIfNeeded];
[UIView animateWithDuration:1.0 animations:^{
self.constraint.constant = 300;
[self.scrollView layoutIfNeeded];
}];
}
When I do this, I get an error in the console. "Unable to simultaneously satisfy constraints." Ending with, "Will attempt to recover by breaking constraint " and it breaks the constraint that I just set for the height of 300.
So....how DO I set the height of that red view to something larger like 300? I assumed if I updated it's height contraint that the contentSize of the scrollView would automatically adjust, but that does not seem to be happening.
You are setting the height of the constraint in code correctly. However, your layout needs some tweaking to get this to work properly.
It looks like you're adding the subviews to the scroll view itself. Instead, you need to add a content view to the scroll view, then add subviews to the content view.
For more information about how to use Auto Layout with a UIScrollView, check out Apple's technical note: developer iOS technotes
In your particular case, I would use Apple's so-called "Mixed Approach". In this approach, you set the content view's frame and the scroll view's content size directly. Calculating the height will be a pain. This involves calculating the height of every individual subview plus margins and spacers.

Using AutoLayout and ensuring bounds of parent view to fit subviews when resized

I really wish I could get my head around auto layout. It seems that whenever I read an abstract description of how things are supposed to work it makes sense, but whenever I actually try and put it into practise it always causes massive headaches. So, apologies if there is already an answer out there for this but I couldn't find one.
The problem should be relatively simple. I have a container view, which contains two subviews, shown here in hideous colours for maximum readability :) :
The bottom (black) view, should remain at it's current size and maintain the spacing between it and the red view, and the spacing between itself and the bottom of yellow view.
The red view I want to be able to dynamically change its height, causing the black view to shift up/down accordingly whilst the yellow view resizes to fit both the red+black views.
For the black view, I've added constraints to:
Set the height to 94
Pin the leading and trailing space to superview
Set the top space to the red view at 51.
Set the bottom space to the yellow view at 20.
I am trying to understand what seemingly-mystical set of constraints I need to add in order that, when the red view is resized vertically, the black view stays its current distance from the red view and maintains its size, and the outer container view resizes accordingly so that it contains the red view + black view + vertical spacing between the views.
For the red view, I've added constraints to pin the top, left and right spacing to superview, but have had no luck working out the vertical constraints. Currently I've got a constraint pinning the height =114 with a priority of 999 and a constraint with height >=114 with a priority of 1000 thinking this would ensure the view is always at least 114 in height...
The fun starts when I try and manually set the height of the red view.... I've added a button on the view, and when the button is pressed, I manually set the bounds of the red view. (The red view's default height is 114):
CGRect bounds = self.redView.bounds;
bounds.size.height = 300;
self.redView.bounds = bounds;
When I run this and press the button, the view goes from this:
To this:
To me this makes no sense whatsoever. Why does this result in:
The Y origin of the red view changing? Particularly when there is a "required" constraint telling it to stay 20pts from the top of yellow view.
The spacing between the red and black views breaking down, even though the constraint on the spacing between them is "required"?
The vertical size of yellow view not changing. Again, despite the spacing between red+black, and me having tried just about every combination I can think of in terms of compression resistance and content hugging priority.....
I really want to understand this, so would be really grateful if someone can explain what additional constraints / changes to constants are required, but more importantly WHY they are required, because to me it doesn't seem clear at all how the layout system comes up with its answers....
Any clarification much appreciated.
Thanks in advance
(All code above is running on iOS 7 and built with Xcode 5.0.2).
You don't need any fancy constraints to do what you want here -- no inequalities or messing with the priorities. In addition to the constraints to the sides, the red view should have 20 to top, 51 to black view, and a height constraint of 114. The black view, has a 20 to the bottom and a height of 94. The superview (yellow) should have zero constraints to top, left and right -- no height. You should have an IBOutlet to the red view's height constraint. When you want to change its height, modify its constraint (don't set frames):
- (IBAction)resizeYellowView:(id)sender {
self.heightCon.constant = 300;
}
Everything is linked together from the top of the yellow view to the bottom with fixed values, so the only thing that can change when the height of the red view changes, is the height of the yellow view.

Resources