148

My understanding of autolayout is that it takes the size of superview and base on constrains and intrinsic sizes it calculates positions of subviews.

Is there a way to reverse this process? I want to resize superview on the base of constrains and intrinsic sizes. What is the simplest way of achieving this?

I have view designed in Xcode which I use as a header for UITableView. This view includes a label and a button. Size of the label differs depending on data. Depending on constrains the label successfully pushes the button down or if there is a constrain between the button and bottom of superview the label is compressed.

I have found a few similar questions but they don’t have good and easy answers.

giampaolo
  • 6,906
  • 5
  • 45
  • 73
DAK
  • 1,505
  • 3
  • 11
  • 5
  • 28
    You should select one of the answers below, as for sure Tom Swifts answered your question. All posters spent a huge amount of time to help you, now you should do your part and select the answer you like the best. – David H Sep 20 '13 at 14:07

4 Answers4

153

The correct API to use is UIView systemLayoutSizeFittingSize:, passing either UILayoutFittingCompressedSize or UILayoutFittingExpandedSize.

For a normal UIView using autolayout this should just work as long as your constraints are correct. If you want to use it on a UITableViewCell (to determine row height for example) then you should call it against your cell contentView and grab the height.

Further considerations exist if you have one or more UILabel's in your view that are multiline. For these it is imperitive that the preferredMaxLayoutWidth property be set correctly such that the label provides a correct intrinsicContentSize, which will be used in systemLayoutSizeFittingSize's calculation.

EDIT: by request, adding example of height calculation for a table view cell

Using autolayout for table-cell height calculation isn't super efficient but it sure is convenient, especially if you have a cell that has a complex layout.

As I said above, if you're using a multiline UILabel it's imperative to sync the preferredMaxLayoutWidth to the label width. I use a custom UILabel subclass to do this:

@implementation TSLabel

- (void) layoutSubviews
{
    [super layoutSubviews];

    if ( self.numberOfLines == 0 )
    {
        if ( self.preferredMaxLayoutWidth != self.frame.size.width )
        {
            self.preferredMaxLayoutWidth = self.frame.size.width;
            [self setNeedsUpdateConstraints];
        }
    }
}

- (CGSize) intrinsicContentSize
{
    CGSize s = [super intrinsicContentSize];

    if ( self.numberOfLines == 0 )
    {
        // found out that sometimes intrinsicContentSize is 1pt too short!
        s.height += 1;
    }

    return s;
}

@end

Here's a contrived UITableViewController subclass demonstrating heightForRowAtIndexPath:

#import "TSTableViewController.h"
#import "TSTableViewCell.h"

@implementation TSTableViewController

- (NSString*) cellText
{
    return @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
}

#pragma mark - Table view data source

- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
    return 1;
}

- (NSInteger) tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger) section
{
    return 1;
}

- (CGFloat) tableView: (UITableView *) tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath
{
    static TSTableViewCell *sizingCell;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        sizingCell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell"];
    });

    // configure the cell
    sizingCell.text = self.cellText;

    // force layout
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    // get the fitting size
    CGSize s = [sizingCell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize];
    NSLog( @"fittingSize: %@", NSStringFromCGSize( s ));

    return s.height;
}

- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
    TSTableViewCell *cell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell" ];

    cell.text = self.cellText;

    return cell;
}

@end

A simple custom cell:

#import "TSTableViewCell.h"
#import "TSLabel.h"

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

- (void) setText: (NSString *) text
{
    _label.text = text;
}

@end

And, here's a picture of the constraints defined in the Storyboard. Note that there are no height/width constraints on the label - those are inferred from the label's intrinsicContentSize:

enter image description here

TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • 1
    Can you give an example of what an implementation of heightForRowAtIndexPath: would look like using this method with a cell containing a multi-line label? I've messed with it quite a bit, and haven't gotten it to work. How do you get a cell (especially if the cell is setup in a storyboard)? What constraints do you need to make it work? – rdelmar Aug 10 '13 at 00:08
  • @rdelmar - sure. I've added an example to my answer. – TomSwift Aug 10 '13 at 17:12
  • Hmm... this is a lot more complicated than my way, and I still haven't gotten it to work properly. The log of fittingSize always gives me (0,0), and I get layout constraint warnings. If I remove the bottom constraint, the warnings go away and I see a cell, but if I return 5 for number of rows, I only see 1. I see you have Content View under the table view cell in your IB image, I don't have that -- what version of Xcode are you using? – rdelmar Aug 10 '13 at 18:44
  • OK, if I do this: [sizingCell systemLayoutSizeFittingSize: UILayoutFittingCompressedSize]; without using ".contentView" it works, and I don't get (0,0) any more. – rdelmar Aug 10 '13 at 18:46
  • @rdelmar - in XCode 4.6 there's a bug where the storyboard editor adds all the constraints to the UITableViewCell itself, not the contentView. The workaround is to manually move all the constraints to the contentView in awakeFromNib. Sorry I forgot about this; I'm using a newer Xcode... – TomSwift Aug 10 '13 at 21:21
  • @rdelmar - your way is how I've done cell-height calculation forever, and it's great until you have a more complicated layout where multiple elements can have dynamic height. This particular question was more about how to get the required size of a superview given subviews with constraints - and systemLayoutSizeFittingSize: is the way to do that. – TomSwift Aug 10 '13 at 21:26
  • Maybe that bug is what is causing the weird results I see. I used a simplified version of your code (no subclassing of the label and no code in the cell.m), and it worked fine with a single label. If I add another label below the first one, with a vertical constraint between them, it returns the same height for every cell no matter the height of the label. But, if I add two labels underneath the multi -line label (spread out x-wise same position y-wise) it worked fine again. Very bizarre. I should try it in Xcode 5. – rdelmar Aug 11 '13 at 00:27
  • 7
    This wasn't working for me until I added a final vertical constraint from the bottom of the cell to the bottom of the lowest subview in the cell. It seems that the vertical constraints must include the top and bottom vertical spacing between the cell and its content for the cell height calculation to succeed. – Eric Baker Jan 08 '14 at 17:56
  • Thank you good sir! I use it in my project. https://github.com/kuchumovn/sociopathy.ios/blob/master/Sociopathy/LibraryViewController.m It's not good that Apple's sdk misses such a basic feature and everyone has to hack it around. – catamphetamine Jan 11 '14 at 17:16
  • Thanks! I wrote a UITableView category using your suggestions, so that one can just call "[tableView sizeHeaderToFit]" and that's all: https://gist.github.com/andreacremaschi/833829c80367d751cb83 – Andrea Cremaschi May 12 '14 at 15:12
  • heightForRowAtIndexPath should be kept as lightweight as possible, as it's called for every cell on the table, whether it's visible or not. – alasker Aug 07 '14 at 15:16
  • Seems to work for me, but I see that longer text content is truncated anyway. Shorter content is fitting perfectly, but labels are sized to up to 9 lines of text. Is there a limit for text label height? – Tomek Cejner Nov 03 '14 at 14:48
  • 1
    I only needed one more trick to get it working. And @EricBaker 's tip finally nailed it. Thanks for sharing that man. I got non-zero size now either against the `sizingCell` or its `contentView`. – MkVal Jan 22 '15 at 09:21
  • 1
    To those having trouble not getting the right height, make sure your `sizingCell`'s width matches your `tableView`s width. – MkVal Jan 22 '15 at 10:36
  • @EricBaker Do you mean add the vertical constraints to the cell or its contentView? – stonedauwg Jan 28 '15 at 21:31
  • @stonedauwg The contentView. Always the contentView. Sorry I was not more clear about that in my original comment. – Eric Baker Apr 02 '15 at 06:30
  • Why is it you use dispatch_once? – Alex Terreaux Feb 19 '16 at 19:37
  • "found out that sometimes intrinsicContentSize is 1pt too short!" - Its the separators to be blamed. We can either turn separators off, or provide extra 1pt to our content height. Otherwise the separator will 'consume' 1pt from your contents height, and content will have height 1pt lesser than they need. – BangOperator Aug 11 '16 at 07:07
30

Eric Baker's comment tipped me off to the core idea that in order for a view to have its size be determined by the content placed within it, then the content placed within it must have an explicit relationship with the containing view in order to drive its height (or width) dynamically. "Add subview" does not create this relationship as you might assume. You have to choose which subview is going to drive the height and/or width of the container... most commonly whatever UI element you have placed in the lower right hand corner of your overall UI. Here's some code and inline comments to illustrate the point.

Note, this may be of particular value to those working with scroll views since it's common to design around a single content view that determines its size (and communicates this to the scroll view) dynamically based on whatever you put in it. Good luck, hope this helps somebody out there.

//
//  ViewController.m
//  AutoLayoutDynamicVerticalContainerHeight
//

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) UIView *contentView;
@property (strong, nonatomic) UILabel *myLabel;
@property (strong, nonatomic) UILabel *myOtherLabel;
@end

@implementation ViewController

- (void)viewDidLoad
{
    // INVOKE SUPER
    [super viewDidLoad];

    // INIT ALL REQUIRED UI ELEMENTS
    self.contentView = [[UIView alloc] init];
    self.myLabel = [[UILabel alloc] init];
    self.myOtherLabel = [[UILabel alloc] init];
    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_contentView, _myLabel, _myOtherLabel);

    // TURN AUTO LAYOUT ON FOR EACH ONE OF THEM
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
    self.myLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.myOtherLabel.translatesAutoresizingMaskIntoConstraints = NO;

    // ESTABLISH VIEW HIERARCHY
    [self.view addSubview:self.contentView]; // View adds content view
    [self.contentView addSubview:self.myLabel]; // Content view adds my label (and all other UI... what's added here drives the container height (and width))
    [self.contentView addSubview:self.myOtherLabel];

    // LAYOUT

    // Layout CONTENT VIEW (Pinned to left, top. Note, it expects to get its vertical height (and horizontal width) dynamically based on whatever is placed within).
    // Note, if you don't want horizontal width to be driven by content, just pin left AND right to superview.
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to left, no horizontal width yet
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to top, no vertical height yet

    /* WHATEVER WE ADD NEXT NEEDS TO EXPLICITLY "PUSH OUT ON" THE CONTAINING CONTENT VIEW SO THAT OUR CONTENT DYNAMICALLY DETERMINES THE SIZE OF THE CONTAINING VIEW */
    // ^To me this is what's weird... but okay once you understand...

    // Layout MY LABEL (Anchor to upper left with default margin, width and height are dynamic based on text, font, etc (i.e. UILabel has an intrinsicContentSize))
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];

    // Layout MY OTHER LABEL (Anchored by vertical space to the sibling label that comes before it)
    // Note, this is the view that we are choosing to use to drive the height (and width) of our container...

    // The LAST "|" character is KEY, it's what drives the WIDTH of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // Again, the LAST "|" character is KEY, it's what drives the HEIGHT of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_myLabel]-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // COLOR VIEWS
    self.view.backgroundColor = [UIColor purpleColor];
    self.contentView.backgroundColor = [UIColor redColor];
    self.myLabel.backgroundColor = [UIColor orangeColor];
    self.myOtherLabel.backgroundColor = [UIColor greenColor];

    // CONFIGURE VIEWS

    // Configure MY LABEL
    self.myLabel.text = @"HELLO WORLD\nLine 2\nLine 3, yo";
    self.myLabel.numberOfLines = 0; // Let it flow

    // Configure MY OTHER LABEL
    self.myOtherLabel.text = @"My OTHER label... This\nis the UI element I'm\narbitrarily choosing\nto drive the width and height\nof the container (the red view)";
    self.myOtherLabel.numberOfLines = 0;
    self.myOtherLabel.font = [UIFont systemFontOfSize:21];
}

@end

How to resize superview to fit all subviews with autolayout.png

John Erck
  • 9,478
  • 8
  • 61
  • 71
  • 3
    This is a great trick and not well known. To repeat: if the inner views have intrinsic height and are pinned to top and bottom, the outer view does not need to specify its height and will in fact hug its contents. You may need to adjust content compression and hugging for the inner views to get desired results. – phatmann Sep 29 '15 at 17:36
  • This is money! One of the better concise VFL examples I've seen. – Evan R Apr 06 '16 at 02:26
  • This is what I was looking for (Thanks @John) so I've posted a Swift version of this [here](https://gist.github.com/docherty/b3229cd099472ca3324e3aaf6a6f7330) – James Jun 17 '16 at 19:00
  • 1
    "You have to choose which subview is going to drive the height and/or width of the container... most commonly whatever UI element you have placed in the lower right hand corner of your overall UI" .. I'm using PureLayout, and this was the key for me. For one parent view, with one child view, it was sitting the child view to pin to the bottom right that suddenly gave the parent view a size. Thanks! – Steven Elliott Jul 06 '17 at 11:29
  • 2
    Choosing one view to drive the width is exactly what I cannot do. Sometimes one subview is wider, sometimes another subview is wider. Any ideas for that case? – Henning Sep 29 '17 at 20:09
  • Thanks for this thorough explanation. This helped me to understand the issue behind and fix it. – Viktor Malyi Oct 10 '17 at 08:41
3

You can do this by creating a constraint and connecting it via interface builder

See explanation: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich beginning-auto-layout

AutolayoutPG Articles constraint Fundamentals

@interface ViewController : UIViewController {
    IBOutlet NSLayoutConstraint *leadingSpaceConstraint;
    IBOutlet NSLayoutConstraint *topSpaceConstraint;
}
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *leadingSpaceConstraint;

connect this Constraint outlet with your sub views Constraint or connect super views Constraint too and set it according to your requirements like this

 self.leadingSpaceConstraint.constant = 10.0;//whatever you want to assign

I hope this clarifies it.

wrtsprt
  • 5,319
  • 4
  • 21
  • 30
chandan
  • 2,453
  • 23
  • 31
  • So it is possible to configure constrains created in XCode from the source code. But the question is how to configure them to resize superview. – DAK Aug 09 '13 at 04:14
  • YES @DAK . please make sure about superview layout. Put Constraint on superview and make height Constraint too so whenever your subview increase it automatically increase superview Constraint height. – chandan Aug 09 '13 at 05:11
  • This also worked for me. It helps that I have fixed sized cells (height of 60px). So when the view loads, I set my IBOutlet'd height constraint to be 60 * x where x is the number of cells. Ultimately, you'd likely want this in a scroll view so that you can see the entire thing. – Sandy Chapman Apr 04 '14 at 10:35
-1

This can be done for a normal subview inside a larger UIView, but it doesn't work automatically for headerViews. The height of a headerView is determined by what's returned by tableView:heightForHeaderInSection: so you have to calculate the height based on the height of the UILabel plus space for the UIButton and any padding you need. You need to do something like this:

-(CGFloat)tableView:(UITableView *)tableView 
          heightForHeaderInSection:(NSInteger)section {
    NSString *s = self.headeString[indexPath.section];
    CGSize size = [s sizeWithFont:[UIFont systemFontOfSize:17] 
                constrainedToSize:CGSizeMake(281, CGFLOAT_MAX)
                    lineBreakMode:NSLineBreakByWordWrapping];
    return size.height + 60;
}

Here headerString is whatever string you want to populate the UILabel, and the 281 number is the width of the UILabel (as setup in Interface Builder)

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • Unfortunately this doesn’t work. “Size to fit content” on the superview removes the constrain which connects the button’s bottom to the superview as you have predicted. At runtime the label is resized and it pushes the button down but the superview is not automatically resized. – DAK Aug 08 '13 at 23:55
  • @DAK, sorry, the problem is that your view is the header for a table view. I misunderstood your situation. I was thinking that the view enclosing the button and label was inside another view, but it sounds like you're using it as the header (rather than being a view inside the header). So, my original answer won't work. I've changed my answer to what I think should work. – rdelmar Aug 09 '13 at 01:05
  • this is similar to my current implementation but I was hoping that there is a better way. I would prefer avoid updating this code every time when I change my header. – DAK Aug 09 '13 at 04:26
  • @Dak, Sorry, I don't think there is a better way, that's just the way table views work. – rdelmar Aug 09 '13 at 04:32
  • Please see my answer. The "better way" is to use UIView systemLayoutSizeFittingSize:. That said, performing a full autolayout on a view or cell just to get the required height in a tableview is fairly expensive. I'd do it for complex cells but perhaps fallback to a solution like yours for simpler cases. – TomSwift Aug 09 '13 at 21:19