Как изменить суперпредставление, чтобы оно соответствовало всем подпредставлениям с autolayout?

142

Мое понимание autolayout состоит в том, что он принимает размер суперпредставления и базируется на ограничениях и внутренних размерах, которые он вычисляет позиции подпредставлений.

Есть ли способ обратить этот процесс вспять? Я хочу изменить размер суперпредставления на основе ограничений и внутренних размеров. Какой самый простой способ достичь этого?

У меня есть представление, разработанное в Xcode, для которого я использую заголовок UITableView. Это представление включает в себя метку и кнопку. Размер этикетки различается в зависимости от данных. В зависимости от ограничений, метка успешно нажимает кнопку вниз или, если между кнопкой и нижней частью супервидения есть ограничение, метка сжимается.

Я нашел несколько похожих вопросов, но у них нет хороших и простых ответов.

ДАК
источник
28
Вы должны выбрать один из ответов ниже, так как наверняка Том Свифтс ответил на ваш вопрос. Все постеры потратили огромное количество времени, чтобы помочь вам, теперь вы должны внести свой вклад и выбрать ответ, который вам нравится больше всего.
Дэвид Х

Ответы:

149

Правильный API для использования UIView systemLayoutSizeFittingSize:, передавая либо UILayoutFittingCompressedSizeили UILayoutFittingExpandedSize.

Для обычного UIViewиспользования autolayout это должно работать, пока ваши ограничения верны. Если вы хотите использовать его в UITableViewCell(например, для определения высоты строки), вы должны вызвать его напротив своей ячейки contentViewи взять высоту.

Дополнительные соображения существуют, если у вас есть один или несколько UILabel по вашему мнению, которые являются многострочными. Для них не обязательно, чтобы preferredMaxLayoutWidthсвойство было установлено правильно, чтобы метка предоставляла правильное значение intrinsicContentSize, которое будет использоваться при systemLayoutSizeFittingSize'sрасчете.

РЕДАКТИРОВАТЬ: по запросу, добавив пример расчета высоты для ячейки табличного представления

Использование autolayout для расчета высоты ячейки таблицы не очень эффективно, но, безусловно, удобно, особенно если у вас есть ячейка со сложным макетом.

Как я уже говорил выше, если вы используете мультилинию UILabel, обязательно синхронизируйте ее preferredMaxLayoutWidthс шириной метки. Я использую пользовательский UILabelподкласс для этого:

@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

Вот надуманный подкласс UITableViewController, демонстрирующий 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

Простая пользовательская ячейка:

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

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

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

@end

И вот картина ограничений, определенных в раскадровке. Обратите внимание, что на метке нет ограничений по высоте / ширине - они выведены из меток intrinsicContentSize:

введите описание изображения здесь

TomSwift
источник
1
Можете ли вы привести пример того, как будет выглядеть реализация heightForRowAtIndexPath: использование этого метода с ячейкой, содержащей многострочную метку? Я немного облажался с этим и не получил его на работу. Как получить ячейку (особенно если ячейка настроена в раскадровке)? Какие ограничения вам нужны, чтобы это работало?
rdelmar
@ rdelmar - конечно. Я добавил пример к своему ответу.
TomSwift
7
Это не работало для меня, пока я не добавил окончательное вертикальное ограничение от нижней части ячейки к нижней части самого нижнего подпредставления в ячейке. Кажется, что вертикальные ограничения должны включать верхний и нижний вертикальный интервал между ячейкой и ее содержимым для успешного расчета высоты ячейки.
Эрик Бейкер
1
Мне нужен был только один трюк, чтобы он заработал. И совет @EricBaker наконец прибил его. Спасибо, что поделились этим человеком. Я получил ненулевой размер сейчас или против sizingCellили его contentView.
MkVal
1
Для тех, кто испытывает затруднения с получением нужной высоты, убедитесь, что ваша sizingCellширина соответствует вашей tableViewширине.
MkVal
30

Комментарий Эрика Бейкера подсказал мне основную идею о том, что для того, чтобы размер представления был определен содержимым, помещенным в него, размещенный в нем контент должен иметь явную связь с содержащим представлением, чтобы повысить его высоту. (или ширина) динамически . «Добавить подпредставление» не создает эти отношения, как вы могли бы предположить. Вы должны выбрать, какое подпредставление будет определять высоту и / или ширину контейнера ... чаще всего любой элемент пользовательского интерфейса, который вы поместили в нижнем правом углу вашего общего пользовательского интерфейса. Вот некоторый код и встроенные комментарии, чтобы проиллюстрировать это.

Обратите внимание, что это может представлять особую ценность для тех, кто работает с представлениями прокрутки, поскольку принято проектировать вокруг одного представления содержимого, которое определяет его размер (и передает это представлению прокрутки) динамически на основе того, что вы в него вставили. Удачи, надеюсь, это поможет кому-то там.

//
//  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

Как изменить суперпредставление, чтобы оно соответствовало всем подпредставлениям с autolayout.png

Джон Эрк
источник
3
Это отличный трюк и малоизвестный. Повторим: если внутренние виды имеют внутреннюю высоту и закреплены сверху и снизу, внешнему виду не нужно указывать его высоту, и он фактически будет обниматься. Возможно, вам придется настроить сжатие содержимого и объятия для внутренних видов, чтобы получить желаемые результаты.
phatmann
Это деньги! Один из лучших сжатых примеров VFL, которые я видел.
Эван Р
Это то, что я искал (Спасибо @Джон), поэтому я разместил здесь
Джеймс
1
«Вы должны выбрать, какое подпредставление будет определять высоту и / или ширину контейнера ... чаще всего любой элемент пользовательского интерфейса, который вы поместили в нижнем правом углу вашего общего пользовательского интерфейса». Я использую PureLayout, и это был ключ для меня. Для одного родительского представления с одним дочерним представлением это было дочернее представление, прикрепленное к нижнему правому углу, которое внезапно дало родительскому представлению размер. Спасибо!
Стивен Эллиотт
1
Выбор одного вида для определения ширины - это то, что я не могу сделать. Иногда одно подпредставление шире, иногда другое подпредставление шире. Есть идеи на этот случай?
Хеннинг
3

Вы можете сделать это, создав ограничение и подключив его через конструктор интерфейса

См. Объяснение: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich начало авто-макета

AutolayoutPG Статьи ограничение Основы

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

соедините этот выход Constraint с вашими подчиненными представлениями Constraint или также подключите суперпросмотры Constraint и настройте их в соответствии с вашими требованиями следующим образом

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

Я надеюсь, что это проясняет это.

Chandan
источник
Таким образом, можно настроить ограничения, созданные в XCode из исходного кода. Но вопрос в том, как их настроить, чтобы изменить размер суперпредставления.
ДАК
ДА @ ДАК. пожалуйста, убедитесь, что о макете superview. Поместите Constraint на суперпредставление и сделайте высоту Constraint также, чтобы всякий раз, когда ваше подпредставление увеличивалось, оно автоматически увеличивало высоту суперпредставления.
Чандан
Это также сработало для меня. Помогает, что у меня есть фиксированные размеры ячеек (высота 60 пикселей). Поэтому, когда представление загружается, я установил ограничение высоты IBOutlet равным 60 * x, где x - количество ячеек. В конечном счете, вы, вероятно, захотите это в виде прокрутки, чтобы вы могли видеть все это.
Сэнди Чепмен,
-1

Это может быть сделано для нормального subviewвнутри большего UIView, но это не работает автоматически headerViews. Высота headerViewопределяется тем , что возвращенное tableView:heightForHeaderInSection:поэтому вы должны рассчитать heightна основе heightиз UILabelплюс пространства для UIButtonи любых paddingвам нужно. Вам нужно сделать что-то вроде этого:

-(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;
}

Вот headerStringлюбая строка, которую вы хотите заполнить UILabel, а число 281 - это widthиз UILabel(как в настройке Interface Builder)

rdelmar
источник
К сожалению, это не работает. «Размер по размеру содержимого» в суперпредставлении снимает ограничение, которое связывает нижнюю часть кнопки с суперпредставлением, как вы и предсказывали. Во время выполнения размер метки изменяется, и она нажимает кнопку вниз, но размер суперпредставления не изменяется автоматически.
DAK
@DAK, извините, проблема в том, что ваше представление является заголовком табличного представления. Я неправильно понял вашу ситуацию. Я думал, что представление, включающее кнопку и метку, было внутри другого представления, но похоже, что вы используете его как заголовок (а не как представление внутри заголовка). Итак, мой оригинальный ответ не сработает. Я изменил свой ответ на то, что я думаю, должно работать.
rdelmar
это похоже на мою текущую реализацию, но я надеялся, что есть лучший способ. Я бы предпочел не обновлять этот код каждый раз, когда я меняю свой заголовок.
ДАК
@Dak, извините, я не думаю, что есть лучший способ, это просто, как работают представления таблицы.
rdelmar
Пожалуйста, посмотрите мой ответ. «Лучшим способом» является использование UIView systemLayoutSizeFittingSize :. Тем не менее, выполнение полной автоматической разметки для представления или ячейки только для получения необходимой высоты в табличном представлении является довольно дорогостоящим. Я бы сделал это для сложных ячеек, но, возможно, отступил бы к решению, как ваше, для более простых случаев.
TomSwift