16
votes

I've not been able to find much in the way of AutoLayout with individual .xib files...

I've got a standalone .xib file that has 3 views - a header view (which contains two labels), an input, and a footer (which contains two buttons). It looks like this:

enter image description here

The labels in the header view have constraints which should affect the vertical size of the header view, and in turn the size of the entire view. The subheader is a label with 0 lines, which means it is multi-line and dynamic. Everything else has a set height with horizontal constraints to superview and top constraints to sibling (or superview in header view's case).

The issue I am having is that when I load this .xib file in code for display, the height is always static based on what is defined in Xcode's inspectors. Is it possible to make the height of the entire view dynamic based on the width (which affects dynamic label height and therefore rest of the view)?

For example - if I load this view from the .xib and set its width to 300, how do I then have it resize its height to accommodate the dynamic label's new height? Do I need to use the intrinsicContentSize method to define this size?

3

3 Answers

17
votes

After much experimentation and reading, I have found the answer. When loading the .xib in some sort of constructor (in my case a class level convenience method), you must make sure to call [view setTranslatesAutoresizingMaskIntoConstraints:NO]; For example, I've done the following:

+ (InputView *)inputViewWithHeader:(NSString *)header subHeader:(NSString *)subHeader inputValidation:(ValidationBlock)validation
{
    InputView *inputView = [[[NSBundle mainBundle] loadNibNamed:@"InputView" owner:self options:nil] lastObject];
    if ([inputView isKindOfClass:[InputView class]]) {
        [inputView setTranslatesAutoresizingMaskIntoConstraints:NO];
        [inputView configureWithHeader:header subHeader:subHeader inputValidation:validation];
        [inputView layoutIfNeeded];
        [inputView invalidateIntrinsicContentSize];
        return inputView;
    }
    return nil;
}

Then, it's necessary to override layoutSubviews and intrinsicContentSize. Overriding layoutSubviews allows me to set the preferredMaxLayoutWidth of my label, while overriding intrinsicContentSize allows me to calculate the size based on constraints and subviews! Here is my implementation of those:

- (void)layoutSubviews {
    [super layoutSubviews];
    self.subHeaderLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds);
    [super layoutSubviews];
}

- (CGSize)intrinsicContentSize {
    CGFloat height = self.headerView.bounds.size.height;
    height += self.headerInputSpacer.constant;
    height += self.inputField.bounds.size.height;
    height += self.inputButtonSpacer.constant;
    height += self.buttonView.bounds.size.height;
    CGSize size = CGSizeMake([UIScreen mainScreen].bounds.size.width - 20, height);
    return size;
}

I'm sure there are ways to improve this, or better ways to make it happen, but for now it is at least sized correctly! Very useful for views that should not have user-defined frames.

3
votes

This is my approach (Swift version):

Embed everything inside a view. Your xib hierarchy should be:

  • Your xib
    • View
      • Everything else, your labels, buttons etc.

View's constraint should constraint to Top, Leading and Trailing to superview(your xib), and inside your "everything else", there should be an object's bottom constraint to the superview (View).

In your code:

Load the nib

nib.frame.size.width = 80 // Set your desired width here

nib.label.text = "hello world" // Set your dynamic text here

nib.layoutIfNeeded() // This will calculate and set the heights accordingly

nib.frame.size.height = nib.view.frame.height + 16 // 16 is the total of top gap and bottom gap of auto layout

For my case, I'm loading this xib into collection view's cell, the xib's height is to be dynamically adjusted, and the width is to follow cell's width. Lastly, the above code block is inside my cellForItemAt method of the collection view.

Hope it helps.

0
votes

Unfortunately, I am not sure what I was missing. The above methods don't work for me to get the xib cell's height or let the layoutifneeded(), UITableView.automaticDimension to do the height calculation. I've been searching and trying for 3 to 4 nights but without an answer. Some answers here or on another post do give me hints for the workaround though. It's a stupid method but it works. Just add all your cells into an Array. And then set the outlet of each of your height constraint in the xib storyboard. Finally, add them up in the heightForRowAt method. It's just straight forward if you are not familiar with the those APIs.

Swift 4.2

CustomCell.Swift

@IBOutlet weak var textViewOneHeight: NSLayoutConstraint!
@IBOutlet weak var textViewTwoHeight: NSLayoutConstraint!
@IBOutlet weak var textViewThreeHeight: NSLayoutConstraint!

@IBOutlet weak var textViewFourHeight: NSLayoutConstraint!
@IBOutlet weak var textViewFiveHeight: NSLayoutConstraint!

MyTableViewVC.Swift

.
.
var myCustomCells:[CustomCell] = []
.
.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = Bundle.main.loadNibNamed("CustomCell", owner: self, options: nil)?.first as! CustomCell

.
.
myCustomCells.append(cell)
return cell

}


override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {

   let totalHeight = myCustomCells[indexPath.row].textViewOneHeight.constant + myCustomCells[indexPath.row].textViewTwoHeight.constant +  myCustomCells[indexPath.row].textViewThreeHeight.constant + myCustomCells[indexPath.row].textViewFourHeight.constant + myCustomCells[indexPath.row].textViewFiveHeight.constant

  return totalHeight + 40 //some magic number


}