What is NSLayoutConstraint "UIView-Encapsulated-Layout-Height" and how should I go about forcing it to recalculate cleanly?

103,333

Solution 1

Try to lower the priority of your _collapsedtextHeightConstraint to 999. That way the system supplied UIView-Encapsulated-Layout-Height constraint always takes precedence.

It is based on what you return in -tableView:heightForRowAtIndexPath:. Make sure to return the right value and your own constraint and the generated one should be the same. The lower priority for your own constraint is only needed temporarily to prevent conflicts while collapse/expand animations are in flight.

Addition: While it may be debatable if the system supplied constraint is correct or not, there's no point in fighting the framework. Simply accept that the system constraint takes precedence. If you think the system constraint is wrong, make sure to return the correct rowHeight from the delegate.

Solution 2

I have a similar scenario: a table view with one row cell, in which there are a few lines of UILabel objects. I'm using iOS 8 and autolayout.

When I rotated I got the wrong system calculated row height (43.5 is much less than the actual height). It looks like:

"<NSLayoutConstraint:0x7bc2b2c0 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7bc37f30(43.5)]>"

It's not just a warning. The layout of my table view cell is terrible - all text overlapped on one text line.

It surprises me that the following line "fixes" my problem magically(autolayout complains nothing and I get what I expect on screen):

myTableView.estimatedRowHeight = 2.0; // any number but 2.0 is the smallest one that works

with or without this line:

myTableView.rowHeight = UITableViewAutomaticDimension; // by itself this line only doesn't help fix my specific problem

Solution 3

99.9% of the time, while using custom cells or headers, all conflicts of UITableViews occur when the table loads of the first time. Once loaded you will usually not see the conflict again.

This happens because most developers typically use a fixed height or anchor constraint of some sort to layout an element in the cell/header. The conflict occurs because when the UITableView first loads/being laid out, it sets the height of its cells to 0. This obviously conflicts with your own constraints. To solve this, simply set any fixed height constraints to a lower priority (.defaultHigh). Read carefully the console message and see which constraint the layout system decided to break. Usually this is the one that needs its priority changed. You can change the priority like this:

let companyNameTopConstraint = companyNameLabel.topAnchor.constraint(equalTo: companyImageView.bottomAnchor, constant: 15)
    companyNameTopConstraint.priority = .defaultHigh

NSLayoutConstraint.activate([
            companyNameTopConstraint,
           the rest of your constraints here
            ])

Solution 4

I was able to get the warning to go away by specifying a priority on one of the values in the constraint the warning messages says it had to break (below "Will attempt to recover by breaking constraint"). It appears that as long as I set the priority to something greater than 49, the warning goes away.

For me this meant changing my constraint the warning said it attempted to break:

@"V:|[contentLabel]-[quoteeLabel]|"

to:

@"V:|-0@500-[contentLabel]-[quoteeLabel]|"

In fact, I can add a priority to any of the elements of that constraint and it will work. It doesn't seem to matter which one. My cells end up the proper height and the warning is not displayed. Roger, for your example, try adding @500 right after the 388 height value constraint (e.g. 388@500).

I'm not entirely sure why this works but I've done a little investigating. In the NSLayoutPriority enum, it appears that the NSLayoutPriorityFittingSizeCompression priority level is 50. The documentation for that priority level says:

When you send a fittingSize message to a view, the smallest size that is large enough for the view's contents is computed. This is the priority level with which the view wants to be as small as possible in that computation. It's quite low. It is generally not appropriate to make a constraint at exactly this priority. You want to be higher or lower.

The documentation for the referenced fittingSize message reads:

The minimum size of the view that satisfies the constraints it holds. (read-only)

AppKit sets this property to the best size available for the view, considering all of the constraints it and its subviews hold and satisfying a preference to make the view as small as possible. The size values in this property are never negative.

I haven't dug beyond that but it does seem to make sense that this has something to do with where the problem lies.

Solution 5

I was able to resolve this error by removing a spurious cell.layoutIfNeeded() that I had in my tableView's cellForRowAt method.

Share:
103,333

Related videos on Youtube

Rog
Author by

Rog

iOS guy valiantly trying to wrestle JavaScript. Also available in short format: sockettrousers but that is really boring.

Updated on July 16, 2021

Comments

  • Rog
    Rog almost 3 years

    I have a UITableView running under iOS 8 and I'm using automatic cell heights from constraints in a storyboard.

    One of my cells contains a single UITextView and I need it to contract and expand based on user input - tap to shrink/expand the text.

    I'm doing this by adding a runtime constraint to the text view and changing the constant on the constraint in response to user events:

    -(void)collapse:(BOOL)collapse; {
    
        _collapsed = collapse;
    
        if(collapse)
            [_collapsedtextHeightConstraint setConstant: kCollapsedHeight]; // 70.0
        else
            [_collapsedtextHeightConstraint setConstant: [self idealCellHeightToShowFullText]];
    
        [self setNeedsUpdateConstraints];
    
    }
    

    Whenver I do this, I wrap it in tableView updates and call [tableView setNeedsUpdateConstraints]:

    [tableView beginUpdates];
    
    [_briefCell collapse:!_showFullBriefText];
    
    [tableView setNeedsUpdateConstraints];
    // I have also tried 
    // [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop];
    // with exactly the same results.
    
    [tableView endUpdates];
    

    When I do this, my cell does expand (and animates whilst doing it) but I get a constraints warning:

    2014-07-31 13:29:51.792 OneFlatEarth[5505:730175] 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) 
    
    (
    
        "<NSLayoutConstraint:0x7f94dced2b60 V:[UITextView:0x7f94d9b2b200'Brief text: Lorem Ipsum i...'(388)]>",
    
        "<NSLayoutConstraint:0x7f94dced2260 V:[UITextView:0x7f94d9b2b200'Brief text: Lorem Ipsum i...']-(15)-|   (Names: '|':UITableViewCellContentView:0x7f94de5773a0 )>",
    
        "<NSLayoutConstraint:0x7f94dced2350 V:|-(6)-[UITextView:0x7f94d9b2b200'Brief text: Lorem Ipsum i...']   (Names: '|':UITableViewCellContentView:0x7f94de5773a0 )>",
    
        "<NSLayoutConstraint:0x7f94dced6480 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7f94de5773a0(91)]>"
     )
    
    Will attempt to recover by breaking constraint 
    
    <NSLayoutConstraint:0x7f94dced2b60 V:[UITextView:0x7f94d9b2b200'Brief text: Lorem Ipsum i...'(388)]>
    

    388 is my calculated height, the other constraints on the UITextView are mine from Xcode/IB.

    The final one is bothering me - I'm guessing that UIView-Encapsulated-Layout-Height is the calculated height of the cell when it is first rendered - (I set my UITextView height to be >= 70.0) however it doesn't seem right that this derived constraint then overrules an updated user cnstraint.

    Worse, although the layout code says it's trying to break my height constraint, it doesn't - it goes on to recalculate the cell height and everything draws as I would like.

    So, what is NSLayoutConstraint UIView-Encapsulated-Layout-Height (I'm guessing it is the calculated height for automatic cell sizing) and how should I go about forcing it to recalculate cleanly?

    • Rog
      Rog over 9 years
      cross posted to Apple dev forums: devforums.apple.com/thread/238803
    • Rog
      Rog over 9 years
      I heard a rumour that auto layout is still changing. Maybe this is a bug.
    • Rog
      Rog over 9 years
      For all those people uprooting, thanks. I would also suggest going to the Apple bug and "me too"ing it to try and bring it to the attention of an Apple dev.
    • Rog
      Rog over 9 years
      My defect report to Apple has been marked as a duplicate which I think means it is a defect.
    • Rog
      Rog over 9 years
      Ortwin's suggestion of lowering the priority works in that it stops the warning but it doesn't have the correct effect on the cell. I think this is an Apple bug and encourage others to create a rdar for it.
    • Jesse
      Jesse over 9 years
      I resolved a similar issue in the following way and it works on iOS 7/8. 1) Lower one of the constraint priorities to 750. I would try the 1st or the 2nd 2) In the cell subclass in awakeFromNib set self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;. I think that initially setting the autoresize mask stops the last constraint from being added. I found this solution here: github.com/wordpress-mobile/WordPress-iOS/commit/…
    • Rog
      Rog over 9 years
      Lowering the priority of one of my constraints simply lets the Apple constraint win which is not what I want - see @Ortwin's answer below. When you set the autoresizingMask set, do you also have translatesAutoresizingMaskIntoConstraints set? Either way, this sounds like mixing two systems - or an Apple bug in Autolayout.
    • orkenstein
      orkenstein over 9 years
      @RogerNolan, any news? I found the same issue while playing with Auto-layout in Interface Builder. Some cells causes this issue, some not.
    • Zhang
      Zhang about 9 years
      Came across this problem myself today. I found that in addition to setting: self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight; I also need to add self.frame = CGRectMake(0, 0, self.frame.size.width, 50); after it and then the problem went away. I posted an answer to a similar question here: stackoverflow.com/questions/25087969/…
    • Rog
      Rog about 9 years
      I don't think that adding an autoresizing mark is a good solution.
    • Ell Neal
      Ell Neal about 9 years
      @RogerNolan are you generating this cell in IB prior to performing these layout changes? I've been debugging the same problem but I wasn't adding any extra constraints. I managed to suppress the warning by rebuilding my view from scratch, and when I diff'ed the 2 storyboard files the only difference was that the version with the warnings was missing the line <rect key="frame" x="0.0" y="0.0" width="600" height="110"/> in its definition, leading me to believe this is an IB bug. At least mine was anyway.
    • Rog
      Rog about 9 years
      I am. I will investigate this thanks.
    • QuangDT
      QuangDT almost 4 years
      I only. get this warning when I set my tableview to editing, so it seems this warning is wrong, like so many many other auto layout warnings.
  • Rog
    Rog over 9 years
    I should have stated in my question, I've tried this and it doesn't work. I agree on your guess about UIView-Encapsulated-Layout-Height
  • Rog
    Rog over 9 years
    Have the bounty anyway for at least submitting an answer. Seems like SO will just let it evaporate otherwise.
  • Rog
    Rog over 9 years
    That would achieve exactly the opposite of what I want. The UIView-Encapsulated-Layout-Height is wrong - it belongs to the previous layout.
  • Ortwin Gentz
    Ortwin Gentz over 9 years
    The UIView-Encapsulated-Layout-Height constraint is added by UITableView once the heights are determined. I calculate the height based on the systemLayoutSizeFittingSize of the contentView. Here, the UIView-Encapsulated-Layout-Height doesn't matter. Then, the tableView sets the contentSize explicitly to the value returned by heightForRowAtIndexPath:. In this case it's correct to lower our custom constraints' priority because the tableView constraint must take precedence after the rowHeights are calculated.
  • Rog
    Rog over 9 years
    @ortwin heightForRowAtIndexPath is olde worlde isn't it? You should not implement it when you have autolayout and self sizing cells.
  • Ortwin Gentz
    Ortwin Gentz over 9 years
    @RogerNolan we had to support iOS 7 as well.
  • testing
    testing over 9 years
    @OrtwinGentz: Still don't get the point it's correct to lower our custom constraints' priority because the tableView constraint must take precedence after the rowHeights are calculated. The thing is that UIView-Encapsulated-Layout-Height is wrong if I don't lower the priority...
  • Rog
    Rog over 9 years
    Thatks Jeff. Still feels like a bug to me. Apple have not responsed to the rdar though :-(
  • Abdalrahman Shatou
    Abdalrahman Shatou over 9 years
    Finally! This is a correct answer. In WWDC session, it was mentioned, if you gonna use the automatic row height sizing, then you shall set an estimatedRowHeight or bad things happens. (Yes, Apple really nasty things happened)
  • ndbroadbent
    ndbroadbent about 9 years
    This doesn't work for me... it just lets iOS break the constraint, so that my cells are shorter than I need them to be. How can I get iOS to recalculate the cell height instead?
  • Ortwin Gentz
    Ortwin Gentz about 9 years
    @nathan.f77 When you change the cell size you have to tell the tableView: [tableView beginUpdates]; [tableView endUpdates]; Then the tableView will ask the delegate via -heightForRowAtIndexPath: for the new heights.
  • ndbroadbent
    ndbroadbent about 9 years
    I'm not really changing the cell heights, I'm trying to reuse an existing cell from dequeueReusableCellWithIdentifier. So the cell height is changing inside my tableView's cellForRowAtIndexPath function. I would just like to get rid of this UIView-Encapsulated-Layout-Height constraint because it doesn't seem to be needed, since the rest of my contraints will lead to the same height. It's just preventing me from updating a reuseable cell's height.
  • Ortwin Gentz
    Ortwin Gentz about 9 years
    Don't fight the framework. UIView-Encapsulated-Layout-Height is automatically added by UITableView. Since this constraint always has a priority of 1000 your vertical constraints should have a lower priority to avoid a conflict.
  • Rog
    Rog about 9 years
    I maintain that's avoiding conflict not fixing the actual problem - although as this still feels like an Apple bug, I think it's probably the right thing to do. Especially in light of the fact that Apple recalculate this constraint and everything does lay out correctly after the error prints.
  • Rachid Finge Jr
    Rachid Finge Jr about 9 years
    Indeed, simply setting the estimatedRowHeight property solved all my problems and thus seems mandatory. It's worth noting that rather than just setting the property to 2.0, it's better to actually estimate the row height for scrolling performance reasons.
  • JRafaelM
    JRafaelM about 9 years
    with Obj-C the only way that fixed the problem was overriding the method from the tableview delegate, thank you a lot! -(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 2.0; }
  • Benjohn
    Benjohn almost 9 years
    FWIW, adding the estimate has made no difference at all for me.
  • andrew k
    andrew k almost 9 years
    -1 for this answer since it assumes the constraint being added is correct. The added UIView-Encapsulated-Layout-Width in my case is just wrong, but it seems to be preferred over my explicit constraints at runtime.
  • Ortwin Gentz
    Ortwin Gentz almost 9 years
    @ray See my updated answer. Don't fight the framework and simply accept that the system constraint takes precedence. If you think the system constraint is wrong, make sure to return the correct rowHeight from the delegate.
  • Benjohn
    Benjohn almost 9 years
    Heh. I'm back at this same answer again, and went to implement it with joy and hope. Once again, it's made no difference for me :-)
  • TigerCoding
    TigerCoding over 8 years
    I think what is happening is the system uses autoresizing mask constraints for a cell content view. So if you specifically set self.translatesAutoresizingMaskIntoConstraints = NO, you interfere with the layout system. Then make sure any constraint changes are done within begin and end updates, and that at least one constraint is 999 as recommended for each axis.
  • Paul T.
    Paul T. over 8 years
    yes, that's the only correct answer for UITableViewAutomaticDimension
  • Fattie
    Fattie over 8 years
    Hi @OrtwinGentz, thanks for the AAA tip. I had a situation where, I was using native select on rows, and then in the cell using setSelected (NOT as is more usual in the view controller) I was expanding or contracting a UILabel by a few lines. In that case the situation is exactly as you describe, as far as I can see. Can't thank you enough. RogerNolan I suspect that the subtle difference you bring to the fore (and I want it explained, too), depends on the situation. Note all readers you must use THIS INCREDIBLE TIP to resolve these issues.
  • Matt
    Matt over 8 years
    Estimates returned by tableView:estimatedHeightForRowAtIndexPath: MUST be at least as large as the cell. Otherwise, the calculated height of the table will be less than the actual height, and the table may scroll up (e.g. after unwind segue returns to the table). UITableViewAutomaticDimension should not be used.
  • frangulyan
    frangulyan about 8 years
    @RogerNolan this answer is not a fix for the problem but rather a dirty workaround. You should not adjust the priorities of your constraints just because there is something else created. Apple docs say that only prio 1000 is required, when you put a lower number then you eventually ask a different layout from the system. And the advice "don't fight the framework" is at least strange. The solution is simple - you just need to provide "estimatedHeightForRowAtIndexPath". In my case making it bigger than the actual height worked. More precisely, return maximum height for your cell's current state.
  • NiñoScript
    NiñoScript about 8 years
    I can confirm that making my constraint's priority be UILayoutPriorityRequired-1 (999) and calling tableView.beginUpdates(); tableView.endUpdates() does the trick.
  • stuckj
    stuckj about 8 years
    Where did you do this? In layoutSubviews?
  • Tjalsma
    Tjalsma almost 8 years
    Here it is a year later, and Apple has not clarified or corrected this cluster-code. Every developer who tries to make a custom keyboard runs into this seemingly simple issue.
  • Stan
    Stan almost 8 years
    Not sure what performance impact this has (none visually), but I found that I still got issues if the intrinsic size of the subviews + constraints was larger than the estimated height. Setting the self.tableView.estimatedRowHeight = 1000 (some arbitrary large number) eliminated the incompatible constraint.
  • Jinghan Wang
    Jinghan Wang over 7 years
    If the gap between estimatedRowHeight and the real height is too large, the tableView may flicker during fast scrolling.
  • Caio
    Caio over 7 years
    Can' add estimatedRowHeight,this make my scroll buggy and makes tableView jump and flicker after any reloadCell or reloadSections.
  • Natalia
    Natalia over 7 years
    if estimatedRowHeight doesn't work, then try tableView.translatesAutoresizingMaskIntoConstraints = false. I was having this constraint issue with a tableview in a container view of a primary view controller that already had the estimatedRowHeight set, and it was not until I made sure it wasn't making constraints from the autoresizing mask that I stopped getting a bunch of constraint errors.
  • Mojo66
    Mojo66 over 7 years
    Note that the lower the estimatedRowHeight the more often cellForRowAtIndexPath will be called initially. tableview height divided by estimatedRowHeight times, to be exact. On an 12 inch iPad Pro, this can potentially be a number in the thousands and will hammer the datasource and can result in a significant delay.
  • Leo
    Leo almost 6 years
    That did help me in a case where I got layout height ambiguities in a pretty complex cell layout only in some simulators (iPad 6 Plus). It appears to me that due to some internal rounding errors the content is a bit squeezed and if the constraints are not prepared to be squeezed than I got ambiguities. So instead of returning UITableViewAutomaticDimension in heightForRowAtIndexPath I return [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize] + 0.1
  • Leo
    Leo almost 6 years
    I mean of course [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].h‌​eight + 0.1
  • John
    John over 5 years
    Yes, this solved the issue for me too. I was doing code layout contraints so I initially thought that I might miss something. Thanks
  • migs647
    migs647 over 5 years
    This was the comment that clued me into my issue. I have an image cell that loads dynamically (grows and shrinks) and a tableview with estimatedRowHeight = 50 and rowHeight = UITableViewAutomaticDimension. I was still breaking on constraints even though the tableview was the correct height. Turns out the separators were 0.3333 in height and that was what was kicking my image size constraint in the cell to be broken. After turning off separators all was well. Thanks Che for giving me what to look for.
  • Anton
    Anton over 5 years
    In this case create an extra constraint, e.g., bottomMargin >= view.bottomMargin+1@900. AutoLayout tries to accommodate the extra 1 point, resizes the cell, gets confused because of the separator height, tries to break/relax some constraints, finds the one @900, and discards that. You get the layout you want without warnings.
  • airowe
    airowe about 5 years
    In Swift 4.2: self.contentView.autoresizingMask = [.flexibleHeight]
  • Andrey Chernukha
    Andrey Chernukha about 5 years
    The same thing! Thanks!
  • Anton Tropashko
    Anton Tropashko about 5 years
    save my day. few hours anyway
  • Enkha
    Enkha almost 5 years
    This worked perfectly for me, even if it was not the perfect solution.
  • royalmurder
    royalmurder over 4 years
    Staggeringly; removing the separator is what got my table behaving, so thanks.
  • FateNuller
    FateNuller about 4 years
    Setting estimatedRowHeight on the table view and then returning automaticDimension from the row height delegate method worked for me. Previously I was not setting the estimatedRowHeight and autolayout was spitting a conflicting constraint message. If you are attempting to set automaticDimension for the height, and adding estimatedRowHeight does not work for you, make sure that the cell you're trying to render has a clear intrinsic content size or else your cell probably will not render correctly.
  • Glenn Posadas
    Glenn Posadas about 4 years
    Beautiful explanation.
  • mschmidt
    mschmidt about 4 years
    I had a similar problem. It worked for me after following these steps: 1) setup tableView.rowHeight to UITableView.automaticDimension, 2) setup tableView.estimatedRowHeight to a sane value, e.g. 44, and 3) update constraints only within updateConstraints before the call to super. I previously updated my constraints in layoutSubviews which seemed to be the cause for the warning in my case.
  • אורי orihpt
    אורי orihpt over 3 years
    Where do you put it?
  • Sourabh Shekhar
    Sourabh Shekhar over 3 years
    Awesome explanation.
  • Lucian Boboc
    Lucian Boboc over 3 years
    Thank you, i struggled with a similar problem today and the only solution that worked was to decrease the bottom priority to 999.
  • Van
    Van over 3 years
    Thank for exact solution and self explanatory answer... saved my day...
  • gngrwzrd
    gngrwzrd about 3 years
    OMG I've tried everything. This finally worked!
  • Frank
    Frank almost 3 years
    If Apple won't fix this problem, it may be called a fix. (But I prefer to call it workaround🤪)
  • surfrider
    surfrider almost 3 years
    That's right, thanks. Worked for me. But I can't undesrstand logic behind that and can't see that's the cell's height is 0 in the moment of this error. The logs in console shows something like this 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7fa661635050.height == 150. It's meaning that the cell's content view got it's height. And for me there is no views with height of 0.
  • Nirav Jain
    Nirav Jain over 2 years
    Thank you!...this is working pretty fine with table view cells as well.
  • Naveen Shan
    Naveen Shan about 2 years
    I tried everything and this works, but I need separator :(
  • K_Mohit
    K_Mohit about 2 years
    Maybe you can add a 1px view in cell itself at the bottom.