Trouble with AutoLayout on UITableViewCell
Asked Answered
S

1

34

I'm having trouble with autolayout on an xcode 5 project. I am using a plain view controller inside with a navigation controller. I have a MKMapView on the top half and a UITableView on the bottom half. I am using storyboards, and have configured the prototype UITableViewCell, but I am adding the constraints through code. I have double-checked every control in the prototype and don't see any constraints configured there. My problem occurs when I add the constraints for the UITableViewCell. I have the following code in the cells:

-(void)updateConstraints {
    [super updateConstraints];
    //first remove old constraints
    [self removeConstraints:self.constraints];
    [self.nameLabel removeConstraints:self.nameLabel.constraints];
    [self.addressLabel removeConstraints:self.nameLabel.constraints];
    [self.rentableSquareFeetLabel removeConstraints:self.rentableSquareFeetLabel.constraints];
    [self.lastSaleAmountLabel removeConstraints:self.lastSaleAmountLabel.constraints];
    [self.lastSaleDateLabel removeConstraints:self.lastSaleAmountLabel.constraints];
    [self.thumbnailImageView removeConstraints:self.thumbnailImageView.constraints];

    //then set up constraints
    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_thumbnailImageView, _nameLabel, _rentableSquareFeetLabel, _lastSaleAmountLabel, _addressLabel, _lastSaleDateLabel);
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_thumbnailImageView(60)]-[_nameLabel(<=200)]-(>=8)-[_rentableSquareFeetLabel]-(>=8)-[_lastSaleAmountLabel]|" options:0 metrics:nil views:viewsDictionary]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_nameLabel]-(-4)-[_addressLabel]" options:NSLayoutFormatAlignAllLeading metrics:nil views:viewsDictionary]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_lastSaleAmountLabel]-(-4)-[_lastSaleDateLabel]" options:NSLayoutFormatAlignAllLeading metrics:nil views:viewsDictionary]];
}

I am getting the following in the debugging console. The exception is triggered by the first addConstraints line. If I just continue through those then eventually everything shows up as it should be, as it looks like xcode is choosing to break the correct constraint:

2013-09-25 15:07:14.169 PECProperties[32381:a0b] Unable to simultaneously satisfy constraints.  Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)  (
    "<NSIBPrototypingLayoutConstraint:0x9d56c70 'IB auto generated at build time for view with fixed frame' H:|-(0)-[UIImageView:0x9d558f0](LTR)   (Names: '|':UITableViewCellContentView:0x9d55620 )>",
    "<NSIBPrototypingLayoutConstraint:0x9d56d20 'IB auto generated at build time for view with fixed frame' H:[UIImageView:0x9d558f0(60)]>",
    "<NSIBPrototypingLayoutConstraint:0x9d56d80 'IB auto generated at build time for view with fixed frame' H:|-(78)-[UILabel:0x9d559e0](LTR)   (Names: '|':UITableViewCellContentView:0x9d55620 )>",
    "<NSLayoutConstraint:0x9d53830 H:[UIImageView:0x9d558f0]-(NSSpace(8))-[UILabel:0x9d559e0]>" )

Will attempt to recover by breaking constraint  <NSIBPrototypingLayoutConstraint:0x9d56d80 'IB auto generated at build time for view with fixed frame' H:|-(78)-[UILabel:0x9d559e0](LTR)   (Names: '|':UITableViewCellContentView:0x9d55620 )>

Break on objc_exception_throw to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

The third NSIBPrototypingLayoutConstraint shows 78 points between the edge of the view and a label. That is where the prototype is positioned roughly (and if I move it in the prototype, I see the change in the constraint in the debugging console), but that conflicts with my own constraint of "standard" distance between the image view and the label.

I have tried setting the translatesAutoresizingMaskIntoConstraints=NO in the view controller's cellForRowAtIndexPath, but that doesn't seem to be helping either. How can I fix the layout?

Stratocracy answered 25/9, 2013 at 21:6 Comment(1)
I noticed an off-by-one bug in iOS7 that caused a similar problem. I was specifying all constraints in IB and XCode was happy. But the sum of all the vertical sizes in the exception < cell height. The solution was the accepted one - to remove one of the constraints at build-time.Mile
G
80

A few things to cover here:

  1. The NSIBPrototypingLayoutConstraint constraints that you're running into (and that are causing exceptions) are auto-generated by Interface Builder in order to make your Storyboard or XIB view layout non-ambiguous. It's pretty sneaky about doing this, but it's automatically adding the minimum constraints required so that the position and size of each ambiguous view becomes fully specified. This is a change from Xcode 4, because in Xcode 4 you could not have ambiguous layouts in Interface Builder. With Xcode 5 and later you can, however IB will auto-generate these constraints for you if your layout is ambiguous at compile time.

    The way to fix this issue is to add the minimum required constraints in Interface Builder so that each view's position & size is fully specified, then select each of these unwanted constraints, go to the right sidebar Attributes inspector, and check the box next to Placeholder - Remove at build time.

    Screenshot of the constraint Placeholder checkbox in Xcode 6.

    Not only does this checkbox remove the constraint you added, but most importantly it will prevent the auto-generated IB constraint from taking its place! (As you can imagine, this is quite tedious when you have a number of views in IB and want to manage all your constraints in code. For this reason you may want to avoid using IB entirely for view hierarchies in which you intend to implement Auto Layout programmatically.)

    What is the difference between a Placeholder constraint and an Uninstalled constraint? Here's a slide from my Adaptive Auto Layout talk (video) (PDF slides) comparing the two:

    Comparison between Placeholder constraints and Uninstalled constraints.

  2. In updateConstraints, you don't want to remove constraints and re-add them like you have there. Why not? Essentially, it's terrible for performance, and I have confirmed with Apple engineers that this is not a good idea. See the question/answer I have posted here for some more details, as well as this answer. In order to prevent constraints being added more than once, use a boolean flag (e.g. hasSetupConstraints) that you set to YES once you have set up your constraints the first time, and if updateConstraints is called again you can just return immediately if you have no new constraints to add. See this question for further discussion.

  3. The code you're using to remove constraints may not work completely. This is because [view removeConstraints:view.constraints] will only remove constraints that have been added to view -- remember that constraints can be added to any common superview of the views they constrain -- and the constraints added to view may not be the only ones affecting the layout of view! If you need to remove a number of constraints, you should store a reference to each of those constraints in a property (e.g. an NSArray property containing NSLayoutConstraint instances), and then deactivate/remove those constraints using the API on NSLayoutConstraint or the PureLayout open-source library. You should only deactivate/remove as few constraints as possible because it is computationally expensive to do so. On the other hand, changing the constant of any constraint is very efficient and encouraged, and you don't need to remove or re-add the constraint to do that.

Gad answered 26/9, 2013 at 5:24 Comment(15)
Thanks so much for your help. I added the constraints in IB, marked them as placeholder, and avoided the exception. In thinking through the rest of your advice, I think there's not really much reason why I can't specify all of these constraints in IB and remove the code altogether. This is my first project with autolayout, and on the view controller I needed to add/remove constraints at rotation, so I was basically trying to take the same approach on the table view cell. But as you pointed out, I don't need to. Thanks again!Stratocracy
No problem. If you can get the constraints working well from IB, go for it. But personally I have basically stopped using IB altogether, especially when it comes to Auto Layout. IB is deceptively simple looking but can cause so much pain for anything but the simplest of view setups (often, what you see is not what you get!). Anyways...feel free to ask if you run across any other stumbling blocks.Gad
I don't understand what you mean by "Not only does this checkbox remove the constraint you added" why would you want to remove a constraint you just added?Zettazeugma
@Zettazeugma If you read the original question, Stephen is adding all his Auto Layout constraints in code instead of from within IB (which is perfectly valid). In order to load the views from a xib/storyboard and do all the constraints in code, you don't want any of the IB constraints around (causing conflicts). And since Xcode will auto-generate IB constraints for ambiguous layouts, you must first add in enough constraints to fully specify all views, then you can click the above checkbox on each constraint. Unfortunately there's no other way to prevent the auto-generated constraints from being added.Gad
So to avoid the auto-generated constraints you first have to create enough constraints for each view for it to be un-ambiguous. Then you have to select each constraint and click that checkbox. Both these steps are to avoid the auto-generated constraints being added.Zettazeugma
@Zettazeugma Exactly! Maybe in the future, IB will have an option that prevents the auto-generated NSIBPrototypingLayoutConstraints from being added to "fix" your ambiguous layouts...would be useful for those who want to use xib/storyboard to set up views, but do some or all of the actual constraints in code.Gad
This option doesn't seem to be available any more under Xcode 6 betas... :/ I wonder what Apple is thinking there. I cannot get them to stop interfering with my layout code!Errhine
@Errhine Using the latest Xcode 6 beta 5, when editing a .xib file in IB, after clicking on a constraint and the Attributes inspector, I still do see the "Placeholder - Remove at build time" checkbox in the same place it shows up in Xcode 5.Gad
Ah, I was using Storyboard. May be there's a difference?Errhine
@Errhine That's possible.Gad
It's absolutely unreal that Apple would consider this a good design. I swear auto layout is one of the worst layout engines I've ever worked with, precisely for reasons like this.Loverly
@Gad you might want to update your Answer since with the latest WWDC, Apple discourages removing constraints. They now Activitate and de-Activate themStreamy
@Jai That is the same thing, and this answer already addresses this...there's nothing to update :)Gad
@Gad ok cool. I only mentioned that because in the WWDC vid, the woman said more then 4 times "do not remove constraints" like you used to do, bad things can happen. She said to instead use the deactivate and activate commands.Streamy
That can also done with intrinsic size property in storyboard then why to use remove at build time against intrinsic size Let me know If i need to ask separate question. but i doubt that ppl downvote it or close itPublus

© 2022 - 2024 — McMap. All rights reserved.