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

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.

Related

Fixed label in that position and equal width

When I was setting a label to equal width inside a scrollview, the label goes up to top instead of staying at the original position. When I remove the constraint, the label is back to position, but without the constraint, the label is not responsive to the screen size.
I already set the scroll view responsive to the view controller. I wished to know the idea of fixing the label position and change the size accordingly to screensize.
Give constrains to your label: left align, right align and Center vertically to superview (in your case ScrollView).
If you can give frame. it will also work else :
Add a top, leading and trailing constraint to the label.
Top constraint will be responsible for your issue of moving to top and left, right constraint will take care of your width of label.

UILabel shifts right when animating superview using auto layout

In the .gif below, I have a view that is the same size as the device screen (with the beige background). That view has a subview (with the purple background), which I've positioned using Auto Layout. It's been set to be the same width as its superview, with a constant value of -18, and its CenterX value set to be the same as its superview, so it's centered with 9pts of space on each side.
Inside of that view, is a UILabel, also positioned using Auto Layout. It's been set to be the same width as its superview, with a constant value of -20, and its CenterX the same as its superview, so it's centered with 10pts of space on each side.
When the user taps on the purple view, I want to perform a multi-step animation, where the first step involves the purple view expanding to be the same width as its superview.
I'm doing this using the following code:
[UIView animateWithDuration:3.0 animations:^{
[constraint setConstant:0.0];
[view.superview layoutIfNeeded];
}];
The problem (as seen in the first .gif), is that the UILabel immediately shifts right, and then gradually comes back to center as the animation plays out.
Incorrect animation:
In this next .gif, however, the animation performs correctly. I'm using the same code to perform the animation, but instead of making the UILLabel be the same width as its superview (minus the 20pts), I hard-code the width to be a static value, using 0 as the constraint multiplier, and 357 as the constant value.
Correct animation:
This accomplishes the effect that I'm looking to achieve, but I'd rather not hard-code the width of the UILabel (plus I'm very curious why it's acting this way).
Any ideas?

Hiding a UIView using AutoLayout constraints

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.

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.

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