3

I am trying to use AutoLayout to configure the subviews in my table view cells and in the ideal case would like the table view cell to be just as high as necessary to contain all the subviews. However, that does not seems to be possible, since the height for a cell is determined before the cell is actually created.

So, for now, I just looked at the constraints I set up and calculated the total height needed to contain everything and set that in the storyboard as the row height for this specific TableViewCell prototype cell and I also return this height in the tableView:heightForRowAtIndexPath: method.

Now, my table view cell looks like this (screenshot from storyboard):

table view cell with auto layout. The two unlabeled constraints have both the size 10

There are two constraints which don't have a size shown, they both have the size 10 (the button-top-container-view and the distance between the slider and the label in the middle).

Going from top-to-bottom the following distance occur:

  • 10 (distance)
  • 50 (height of button)
  • 20 (distance)
  • 20 (height of label)
  • 10 (distance)
  • 30 (height of slider)
  • 20 (distance)

leading to a total height of 160.

That's exactly what I have set in the inspector of this cell:

inspector of table view cell showing height of the table view cell as 160

but as you can see from the first screenshot, Interface builder complains that the constraints are conflicting (that's why they are displayed in red). IB is satisfied if I set the height to 161, but that is wrong.

Also, if I do, I get exceptions at runtime due to conflicting constraints:

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:0x8d80240 V:[UISlider:0x8d81190(30)]>",
"<NSLayoutConstraint:0x8d833e0 V:[UILabel:0x8d83300(20)]>",
"<NSLayoutConstraint:0x8d83750 V:[UIButton:0x8d83600(50)]>",
"<NSLayoutConstraint:0x8d85050 V:|-(10)-[UIButton:0x8d83600]   (Names: '|':UITableViewCellContentView:0x8d80ef0 )>",
"<NSLayoutConstraint:0x8d851d0 V:[UILabel:0x8d83300]-(10)-[UISlider:0x8d81190]>",
"<NSLayoutConstraint:0x8d85200 V:[UISlider:0x8d81190]-(20)-|   (Names: '|':UITableViewCellContentView:0x8d80ef0 )>",
"<NSLayoutConstraint:0x8d85320 V:[UIButton:0x8d83600]-(20)-[UILabel:0x8d83300]>",
"<NSAutoresizingMaskLayoutConstraint:0x8d8b7a0 h=--& v=--& V:[UITableViewCellContentView:0x8d80ef0(161)]>"

)

the last one is the only one I did not explicitly set, but it seems to be generated from the row-height setting in the storyboard, since my tableView:heightForRowAtIndexPath: method returns 160:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.row == self.fetchedItemSetsController.fetchedObjects.count - 1)
    {
        return 160;
    }
    return 44;
}

(at the moment I always want the last cell to be the large and special one)

So, okay, IB complains when I set the row height to 160, but when I set it to 161 the runtime complains. So I tried ignoring IB and set the row height to 160 in the storyboard as well (as seen in the second screenshot). In that case, I get the same error message with a tiny difference:

"<NSLayoutConstraint:0x17809ce80 V:[UISlider:0x125613660(30)]>",
"<NSLayoutConstraint:0x1782814f0 V:[UILabel:0x125615100(20)]>",
"<NSLayoutConstraint:0x178281a90 V:[UIButton:0x125615b50(50)]>",
"<NSLayoutConstraint:0x178281b80 V:|-(10)-[UIButton:0x125615240]   (Names: '|':UITableViewCellContentView:0x178161980 )>",
"<NSLayoutConstraint:0x178281c20 UIButton:0x1256157b0.height == UIButton:0x125615240.height>",
"<NSLayoutConstraint:0x178281d10 UIButton:0x1256157b0.height == UIButton:0x125615980.height>",
"<NSLayoutConstraint:0x178281e00 V:[UILabel:0x125615100]-(10)-[UISlider:0x125613660]>",
"<NSLayoutConstraint:0x178281e50 V:[UISlider:0x125613660]-(20)-|   (Names: '|':UITableViewCellContentView:0x178161980 )>",
"<NSLayoutConstraint:0x178281f40 UIButton:0x125615980.height == UIButton:0x125615b50.height>",
"<NSLayoutConstraint:0x178282030 V:[UIButton:0x125615240]-(20)-[UILabel:0x125615100]>",
"<NSAutoresizingMaskLayoutConstraint:0x178283340 h=--& v=--& V:[UITableViewCellContentView:0x178161980(159.5)]>"

The constraint for the height of the table view cell is now 159.5 instead of 160. I also encountered it as 160.5 before, with the same storyboard setting, but I am not sure where that came from.

So, first I thought it was just a display bug in IB, but now it actually seems to be creating the wrong constraint. It creates a constraint with 160.5 (or 159.5) instead of the 160 I specified. Why is that? What can I do about it?

Btw: The cells seems to be displayed correctly, but then again I doubt I would be able to see a 0.5 point difference. Mainly, I would like to get rid of the exceptions, since they make the debugging much harder, but I would also like to know what is going on here.

UPDATE: The 159.5 is not JUST due to the storyboard setting I just noticed, it is a strange combination of the storyboard setting and the return value of the tableView:heightForRowAtIndexPath:. Here are a few examples of the height of the generated autoresizingmasklayoutconstraint depending on the storyboard height (sb) setting and the return value of the tableView:heightForRowAtIndexPath: method (method). In all cases, the constraints of the subview should lead to a height of 160 and are not changed.

  1. 160 (sb) & 160 (method): 159.5 (constraint)
  2. 161 (sb) & 160 (method): 159.5 (constraint)
  3. 160 (sb) & 161 (method): 160.5 (constraint)
  4. 162 (sb) & 161 (method): 162 (constraint)
  5. 161 (sb) & 162 (method): 161 (constraint)
  6. 162 (sb) & 162 (method): 162 (constraint)

So I thought, okay, for (6.) they all agree, I will just make the label's height 22, so the total height according to the subviews will be 162 as well. Result:

6a. 162 (sb) & 162 (method), 162 (total height of subviews): 161.5 (constraint)

WHAT?!

Any ideas what's happening here?

Update 2 I provided an example project that reproduces the issue on Github.

Joachim Kurz
  • 2,875
  • 6
  • 23
  • 43
  • Any chance to get the storyboard and other required files to reproduce it ? – A-Live Mar 20 '14 at 17:22
  • I'll see whether I can reproduce it in a smaller example project. – Joachim Kurz Mar 20 '14 at 17:27
  • I created a small example project. What would be the best and easiest way to upload it and keep it around as long as this question lives? – Joachim Kurz Mar 20 '14 at 17:49
  • Stick it on github. This allows others to play with it and submit changes of their own. – matt Mar 20 '14 at 17:50
  • @A-Live I added an example project that has the same problem. – Joachim Kurz Mar 20 '14 at 18:19
  • @Joachim Kurz, thanks, I noticed that the content view height is shown as `159` at interface builder and decided to disable standard table view separator, it actually helped: the content view height became `160` and the layout errors disappeared. Great question ! – A-Live Mar 24 '14 at 11:46

2 Answers2

4

Now that you've uploaded the example to github, I was able to see that the constraints were in fact over-determined. There was too much "this is equal to that", esp. in the area of vertical spacings/heights. I simplified the constraints, and was able to change to your code to this:

-(CGFloat)tableView:(UITableView *)tableView 
    heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.row == 0)
    {
        return 44;
    } // else
    UITableViewCell* cell = 
        [tableView dequeueReusableCellWithIdentifier:@"LargeCell"];
    CGSize sz = 
        [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingExpandedSize];
    return sz.height;
}

If I'm not mistaken, that's exactly the sort of thing you are after, i.e. to let the constraints themselves dictate the height of the cell.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • thanks for posting a complete example of how to use `systemLayoutFittingSize` correctly, that was my problem when I looked at some of the other questions and your book's example. I accepted @A-Live's answer since it gives a better explanation of the why. Yours is a better explanation of the how, though. Thanks for the interesting discussion. – Joachim Kurz Mar 24 '14 at 13:11
  • @JoachimKurz In my view (not that it's any of my business), you are quite right to accept A-live's answer. I didn't mention the separator because that didn't occur to me! (Indeed, I took down my "wiggle room" answer because I felt, along with you, that it wasn't satisfying.) You've really taken us down a fascinating road here and I learned a lot. – matt Mar 24 '14 at 16:22
  • @JoachimKurz This is great. In https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch08p424variableHeights/ch21p722variableHeights/RootViewController.m I was adding 1 to the constraint-generated cell height and not understanding why. Now I do! It's the separator!! – matt Mar 24 '14 at 17:54
  • +1 for this, although I found I didn't need to use `UILayoutFittingExpandedSize` - you can pass your own size and use the variant that allows you to pass layout priorities: `systemLayoutSizeFittingSize: size withHorizontalFittingPriority: UILayoutPriorityRequired verticalFittingPriority: 1`. One VERY important thing I missed and only noticed today in this answer is that you should use the contentView of the cell, NOT the cell itself. We were always getting 0.5pt too high in the returned height and this was causing a lot of ambiguous layouts. 3 months of pain gone, thanks! – Sam Jun 13 '17 at 14:29
4

Separator is messing with the height of the contentView, either it should be disabled (and replaced with custom one if required) so that the contentView height matches the height of the cell or the constraints should be changed to more flexible considering that the contentView height may mismatch the height of the cell. Doing both would be even better.

A-Live
  • 8,904
  • 2
  • 39
  • 74
  • ah, the separator is the culprit! I really couldn't understand why the constraints, although calculated to match the content height wouldn't fit at runtime, but it makes sense that the separator takes away part of the content view's available height. It's still strange, how the constraints behave for different settings of the cell height in interface builder and the delegate call, though. Instead of making the constraints more flexible, one can use @matt's answer to calculate the correct height of the cell dynamically. – Joachim Kurz Mar 24 '14 at 13:15
  • "Strange" results of different combinations of SB and delegate values may be caused by automatic attempt to resolve the layout issue. Although the values from delegate method should have higher priority, the constraints must be pre-analysed from the SB (as that is the only place you configure them) and different values there may give different analysis results. Such auto-repair process is likely to be quite random for those who don't know implementation, but we have the warnings when it happens so I guess it's fine. – A-Live Mar 24 '14 at 14:12
  • @A-Live The separator!!!! You just made a huge lightbulb come on in my head. Thanks so much. – matt Mar 24 '14 at 17:54