Autolayout height changes when I push a new view controller - ios

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.

Related

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?

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.

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.

How to scroll to bottom of UITableView to see all cell contents without adjusting cell height?

I am having an issue with my UITableView not being able to scroll past the last cell. It just "bounces" back up. Depending on what iPhone simulation I run, it will bounce back up past 2 cells, so if I run it on a 3.5 inch simulator, the last 2 cells are not reachable.
I have constraints setup with AutoLayout enabled, which is a requirement for me. I have looked at some suggestions on SO, trying some solutions, but so far, it does not work.
I prefer not to set the cell height, if possible.
In the scrolled state when I scroll up, I can see 2 more cells, but once I release the mouse hold, it bounces back up to the normal state with the 2 last cells not in view.
How do I allow the UITableView to be scrolled in a small window frame without adjusting the height, and still be able to see all cell contents?
Several problems:
The constraints you had set up were showing warnings. According to you app, you need to fix it by your self;
The bottom cell wasn't visible that is because of the constraints' issue.
In you swipeUpMethod method, you need to adjust your constraints. In this case, I set self.markerViewTopVerticalConstraint.constant to -(self.view.frame.size.height/2.5) - 60. So you need to calculate the constraint height according to the number of cells which will display in your tableView.
- (void)swipeUpMethod
{
[self.view layoutIfNeeded];
[UIView animateWithDuration:0.3
animations:^{
self.markerViewTopVerticalConstraint.constant = -(self.view.frame.size.height/2.5) - 60;
[self.view layoutIfNeeded];
}];
self.swipeMarkerImageView.image = [UIImage imageNamed:#"down_arrow"];
NSLog(#"self.markersView: %#",self.markersView);
NSLog(#"self.markersTableView :%#",self.markersTableView);
}
Now, it looks like:
This is an autolayout problem. Make sure that the autolayout is set correctly. As far as I can understand from seeing your snapshot, the map view's height is constant and the table view's height is changing as per device height. Now to correctly set the autolayout, select the map view and add the following constraints, top, left, Width and height. Make sure to uncheck the "Constrain to margins". Now for the table view use the following auto layout. Add top, bottom left and right constraints.
After this, please check in other simulators. Hoppe this helps.

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.

Resources