Можно ли использовать AutoLayout с UITableView tableHeaderView?


С AutoLayout Я использую его везде, теперь я пытаюсь использовать его с tableHeaderView.

я subclass на UIView добавлены все (этикетки и т. д...) Я хотел с их ограничениями, то я добавил Это CustomView до UITableView'tableHeaderView.

все работает просто отлично, за исключением UITableView всегда отображается выше the CustomView, by выше я имею в виду CustomView и под the UITableView так что это не видно !

кажется, что независимо от того, что я делаю,height на UITableView'tableHeaderView - это всегда 0 (так это ширина, x и y).

мой вопрос : возможно ли вообще это сделать без установки рамки вручную ?

EDIT: Элемент CustomView'subview что я использую имеет следующие ограничения :

_title = [[UILabel alloc]init];
_title.text = @"Title";
[self addSubview:_title];
[_title keep:[KeepTopInset rules:@[[KeepEqual must:5]]]]; // title has to stay at least 5 away from the supperview Top
[_title keep:[KeepRightInset rules:@[[KeepMin must:5]]]];
[_title keep:[KeepLeftInset rules:@[[KeepMin must:5]]]];
[_title keep:[KeepBottomInset rules:@[[KeepMin must:5]]]];

я использую удобную библиотеку "KeepLayout", потому что пишу ограничения вручную занимает вечность и слишком много строк для одного ограничения, но методы самоописания.

и UITableView имеет следующие ограничения :

_tableView = [[UITableView alloc]init];
_tableView.translatesAutoresizingMaskIntoConstraints = NO;
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.backgroundColor = [UIColor clearColor];
[self.view addSubview:_tableView];
[_tableView keep:[KeepTopInset rules:@[[KeepEqual must:0]]]];// These 4 constraints make the UITableView stays 0 away from the superview top left right and bottom.
[_tableView keep:[KeepLeftInset rules:@[[KeepEqual must:0]]]];
[_tableView keep:[KeepRightInset rules:@[[KeepEqual must:0]]]];
[_tableView keep:[KeepBottomInset rules:@[[KeepEqual must:0]]]];

_detailsView = [[CustomView alloc]init];
_tableView.tableHeaderView = _detailsView;

я не знаю, если я должен установить некоторые ограничения непосредственно на CustomView, Я думаю, что высота CustomView определяется ограничениями на UILabel "название" в нем.

EDIT 2: после другого исследования кажется высота и ширина CustomView правильно рассчитаны, но верхняя часть CustomView по-прежнему находится на том же уровне, что и верхняя часть UITableView, и они перемещаются вместе при прокрутке.

23 82

23 ответа:

Я задал и ответил на аналогичный вопрос здесь. Таким образом, я добавляю заголовок один раз и использую его, чтобы найти необходимую высоту. Затем эта высота может быть применена к заголовку, и заголовок устанавливается во второй раз, чтобы отразить изменение.

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.header = [[SCAMessageView alloc] init];
    self.header.titleLabel.text = @"Warning";
    self.header.subtitleLabel.text = @"This is a message with enough text to span multiple lines. This text is set at runtime and might be short or long.";

    //set the tableHeaderView so that the required height can be determined
    self.tableView.tableHeaderView = self.header;
    [self.header setNeedsLayout];
    [self.header layoutIfNeeded];
    CGFloat height = [self.header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    //update the header's frame and set it again
    CGRect headerFrame = self.header.frame;
    headerFrame.size.height = height;
    self.header.frame = headerFrame;
    self.tableView.tableHeaderView = self.header;
}

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

- (void)layoutSubviews
{
    [super layoutSubviews];

    self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame);
    self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame);
}

или, возможно, в более общем плане:

override func layoutSubviews() {
    super.layoutSubviews()  
    for view in subviews {
        guard let label = view as? UILabel where label.numberOfLines == 0 else { continue }
        label.preferredMaxLayoutWidth = CGRectGetWidth(label.frame)
    }
}

Обновление Января 2015

к сожалению, это все еще кажется необходимым. Вот быстрая версия процесса компоновки:

tableView.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
header.frame.size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
tableView.tableHeaderView = header

Я нашел полезным переместить это в расширение на UITableView:

extension UITableView {
    //set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
    func setAndLayoutTableHeaderView(header: UIView) {
        self.tableHeaderView = header
        header.setNeedsLayout()
        header.layoutIfNeeded()
        header.frame.size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
        self.tableHeaderView = header
    }
}

использование:

let header = SCAMessageView()
header.titleLabel.text = "Warning"
header.subtitleLabel.text = "Warning message here."
tableView.setAndLayoutTableHeaderView(header)

мне не удалось добавить представление заголовка с помощью ограничений (в коде). Если я даю моему виду ограничение ширины и / или высоты, я получаю сбой с сообщением:

 "terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super."

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

ширина автоматически ширина табличного представления, единственное, что вам нужно установить, это высота-исходные значения игнорируются, поэтому не имеет значения, что вы вкладываете в них. Например, это работало нормально (как и 0,0,0,80 для прямой кишки):

UIView *headerview = [[UIView alloc] initWithFrame:CGRectMake(1000,1000, 0, 80)];
headerview.backgroundColor = [UIColor yellowColor];
self.tableView.tableHeaderView = headerview;

Я видел много методов здесь делать так много ненужных вещей, но вам не нужно так много, чтобы использовать auto layout в представлении заголовка. Вам просто нужно создать xib-файл, поместить свои ограничения и создать его следующим образом:

func loadHeaderView () {
        guard let headerView = Bundle.main.loadNibNamed("CourseSearchHeader", owner: self, options: nil)?[0] as? UIView else {
            return
        }
        headerView.autoresizingMask = .flexibleWidth
        headerView.translatesAutoresizingMaskIntoConstraints = true
        tableView.tableHeaderView = headerView
    }

другое решение-отправить создание представления заголовка в следующий вызов основного потока:

- (void)viewDidLoad {
    [super viewDidLoad];

    // ....

    dispatch_async(dispatch_get_main_queue(), ^{
        _profileView = [[MyView alloc] initWithNib:@"MyView.xib"];
        self.tableView.tableHeaderView = self.profileView;
    });
}

Примечание: это исправить ошибку, когда загруженный вид имеет фиксированную высоту. Я не пробовал, когда высота заголовка зависит только от его содержимого.

EDIT:

вы можете найти более чистое решение этой проблемы, реализовав это функции, и viewDidLayoutSubviews

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    [self sizeHeaderToFit];
}

расширил это решение http://collindonnell.com/2015/09/29/dynamically-sized-table-view-header-or-footer-using-auto-layout/ для просмотра нижнего колонтитула таблицы:

@interface AutolayoutTableView : UITableView

@end

@implementation AutolayoutTableView

- (void)layoutSubviews {
    [super layoutSubviews];

    // Dynamic sizing for the header view
    if (self.tableHeaderView) {
        CGFloat height = [self.tableHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect headerFrame = self.tableHeaderView.frame;

        // If we don't have this check, viewDidLayoutSubviews() will get
        // repeatedly, causing the app to hang.
        if (height != headerFrame.size.height) {
            headerFrame.size.height = height;
            self.tableHeaderView.frame = headerFrame;
            self.tableHeaderView = self.tableHeaderView;
        }

        [self.tableHeaderView layoutIfNeeded];
    }

    // Dynamic sizing for the header view
    if (self.tableFooterView) {
        CGFloat height = [self.tableFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect footerFrame = self.tableFooterView.frame;

        // If we don't have this check, viewDidLayoutSubviews() will get
        // repeatedly, causing the app to hang.
        if (height != footerFrame.size.height) {
            footerFrame.size.height = height;
            self.tableFooterView.frame = footerFrame;
            self.tableFooterView = self.tableFooterView;
        }

        self.tableFooterView.transform = CGAffineTransformMakeTranslation(0, self.contentSize.height - footerFrame.size.height);
        [self.tableFooterView layoutIfNeeded];
    }
}

@end

странные вещи происходит. systemLayoutSizeFittingSize отлично подходит для iOS9, но не для iOS 8 в моем случае. Так что эту проблему решить довольно легко. Просто получить ссылку на вид снизу в заголовке и в viewDidLayoutSubviews после супер вызова обновить границы представления заголовка, вставив высоту как CGRectGetMaxY (yourview.рамка) + обивка

UPD: самое простое решение когда-либо: Итак, в представлении заголовка поместите подвиды и прикрепите его к левый,право, top. В этом подвиде разместите свои подвиды с ограничениями автоматической высоты. После этого отдайте все задание на автоотчет (расчет не требуется)

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    CGFloat height = CGRectGetMaxY(self.tableView.tableHeaderView.subviews.firstObject.frame);
    self.tableView.tableHeaderView.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds), height);
    self.tableView.tableHeaderView = self.tableView.tableHeaderView;
}

в результате subview расширяется / сжимается, как и должно быть, в конце он вызывает viewDidLayoutSubviews. В то время мы знаем фактический размер представления, поэтому установите headerview height и обновите его путем повторного назначения. Работает как шарм!

также работает для нижнего колонтитула.

вы можете получить autolayout, чтобы предоставить вам размер с помощью systemLayoutSizeFittingSize метод.

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

код в swift выглядит как

//Create the view
let tableHeaderView = CustomTableHeaderView()

//Set the content
tableHeaderView.textLabel.text = @"Hello world"

//Ask auto layout for the smallest size that fits my constraints    
let size = tableHeaderView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

//Create a frame    
tableHeaderView.frame = CGRect(origin: CGPoint.zeroPoint, size: size)

//Set the view as the header    
self.tableView.tableHeaderView = self.tableHeaderView

или в Objective-C

//Create the view
CustomTableHeaderView *header = [[CustomTableHeaderView alloc] initWithFrame:CGRectZero];

//Set the content
header.textLabel.text = @"Hello world";

//Ask auto layout for the smallest size that fits my constraints
CGSize size = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

//Create a frame
header.frame = CGRectMake(0,0,size.width,size.height);

//Set the view as the header  
self.tableView.tableHeaderView = header

следует также отметить, что в данном конкретном случае, переопределение requiresConstraintBasedLayout в вашем подклассе приводит к выполнению прохода макета, однако результаты этого прохода макета игнорируются, а системный фрейм устанавливается на ширину tableView и высоту 0.

код:

  extension UITableView {

          func sizeHeaderToFit(preferredWidth: CGFloat) {
            guard let headerView = self.tableHeaderView else {
              return
            }

            headerView.translatesAutoresizingMaskIntoConstraints = false
            let layout = NSLayoutConstraint(
              item: headerView,
              attribute: .Width,
              relatedBy: .Equal,
              toItem: nil,
              attribute:
              .NotAnAttribute,
              multiplier: 1,
              constant: preferredWidth)

            headerView.addConstraint(layout)

            let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
            headerView.frame = CGRectMake(0, 0, preferredWidth, height)

            headerView.removeConstraint(layout)
            headerView.translatesAutoresizingMaskIntoConstraints = true

            self.tableHeaderView = headerView
          }
  }

следующее работало для меня.

  1. использовать простой старый UIView Как посмотреть заголовок.
  2. добавить подвиды к этому UIView
  3. используйте автозапуск на подвидах

основное преимущество, которое я вижу, это ограничение вычислений кадров. Apple действительно должна обновить UITableViewAPI, чтобы сделать это проще.

пример использования SnapKit:

let layoutView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 60))
layoutView.backgroundColor = tableView.backgroundColor
tableView.tableHeaderView = layoutView

let label = UILabel()
layoutView.addSubview(label)
label.text = "I'm the view you really care about"
label.snp_makeConstraints { make in
    make.edges.equalTo(EdgeInsets(top: 10, left: 15, bottom: -5, right: -15))
}

мое представление заголовка таблицы является подклассом UIView - я создал один contentView UIView в инициализаторе, с его границами, такими же, как фрейм представления заголовка таблицы, и добавил Все мои объекты в качестве подвида этого.

затем добавьте ограничения для ваших объектов в представлении заголовка таблицы layoutSubviews метод, а не внутри инициализатора. Это решило проблему аварии.

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:CGRectMake(0, 0, 0, 44.0)];
    if (self) {
        UIView *contentView = [[UIView alloc] initWithFrame:self.bounds];
        contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth;

        // add other objects as subviews of content view

    }
    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    // remake constraints here
}

вы можете добавить ограничение top + horizontal location между заголовком и tableview, чтобы разместить его правильно (если сам заголовок содержит все необходимые внутренние ограничения макета, чтобы иметь правильный кадр)

в tableViewController методе viewDidLoad

    headerView.translatesAutoresizingMaskIntoConstraints = false

    tableView.tableHeaderView = headerView

    headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true
    headerView.topAnchor.constraint(equalTo: tableView.topAnchor).isActive = true
    headerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor).isActive = true

Я знаю, что это старый пост, но после прохождения всех сообщений SO относительно этого и прохождения целого дня, играя с этим, я, наконец, придумал чистое и все же очень простое решение

прежде всего, моя иерархия представления выглядит так:

  1. Посмотреть В Таблице
    1. View tableHeaderView
      1. View С розеткой под названием headerView

теперь внутри представления (№3) я настроил все ограничения, как обычно, включая нижнее пространство для контейнера. Это сделает контейнер (т. е. 3.View т. е. headerView) для определения размера на основе его подвидов и их ограничений.

после этого я устанавливаю ограничения между 3. View и 2. View на эти:

  1. верхний космос к контейнеру: 0
  2. ведущий космос к контейнеру: 0
  3. конечное пространство для контейнера: 0

обратите внимание, что я намеренно опускаю нижнее пространство намеренно.

после того, как все это делается в раскадровке, все, что осталось сделать, это вставить этот код:

if (self.headerView.frame.size.height != self.tableView.tableHeaderView.frame.size.height) {
    UIView *header = self.tableView.tableHeaderView;
    CGRect frame = self.tableView.tableHeaderView.frame;
    frame.size.height = self.headerView.frame.size.height + frame.origin.y;
    header.frame = frame;
    self.tableView.tableHeaderView = header;
}

советы: Если вы используете метод setAndLayoutTableHeaderView, вы должны обновить фрейм вложенных представлений, поэтому в этой ситуации предпочтительный параметр Uilabelmaxlayoutwidth должен вызывать до вызова systemLayoutSizeFittingSize, не вызывайте layoutSubview.

показать код

поделиться своим подходом.

UITableView+XXXAdditions.m

- (void)xxx_setTableHeaderView:(UIView *)tableHeaderView layoutBlock:(void(^)(__kindof UIView *tableHeaderView, CGFloat *containerViewHeight))layoutBlock {
      CGFloat containerViewHeight = 0;
      UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectZero];
      [backgroundView addSubview:tableHeaderView];
      layoutBlock(tableHeaderView, &containerViewHeight);

      backgroundView.frame = CGRectMake(0, 0, 0, containerViewHeight);

      self.tableHeaderView = backgroundView;
}

использование.

[self.tableView xxx_setTableHeaderView:myView layoutBlock:^(__kindof UIView * _Nonnull tableHeaderView, CGFloat *containerViewHeight) {
    *containerViewHeight = 170;

    [tableHeaderView mas_makeConstraints:^(MASConstraintMaker *make) {
      make.top.equalTo(@20);
      make.centerX.equalTo(@0);
      make.size.mas_equalTo(CGSizeMake(130, 130));
    }];
  }];

в моем случае метод с systemLayoutSizeFittingSize почему-то не работал. То, что сработало для меня, - это модификация решения, опубликованного HotJard (его оригинальное решение также не работало в моем случае на iOS 8). То, что мне нужно было сделать, это в представлении заголовка поместить подвида и закрепить его слева, справа, сверху (не закреплять снизу). Поместите все с помощью autolayout в этом подвиде и в коде сделать это:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    [self resizeHeaderToFitSubview];
}

- (void)resizeHeaderToFitSubview
{
    UIView *header = self.tableView.tableHeaderView;
    [header setNeedsLayout];
    [header layoutIfNeeded];
    CGFloat height = CGRectGetHeight(header.subviews.firstObject.bounds);
    header.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds), height);
    self.tableView.tableHeaderView = nil;
    self.tableView.tableHeaderView = header;
}

старый пост. Но хороший пост. Вот мои 2 цента.

во-первых, убедитесь, что ваше представление заголовка имеет свои ограничения, расположенные так, чтобы оно могло поддерживать свой собственный внутренний размер контента. Затем сделайте следующее.

//ViewDidLoad
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.configure(title: "Some Text A")

//Somewhere else
headerView.update(title: "Some Text B)

private var widthConstrained = false

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if widthConstrained == false {
        widthConstrained = true
        tableView.addConstraint(NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: tableView, attribute: .width, multiplier: 1, constant: 0))
        headerView.layoutIfNeeded()
        tableView.layoutIfNeeded()
    }
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate(alongsideTransition: { (context) in
        self.headerView.layoutIfNeeded()
        self.tableView.layoutIfNeeded()
    }, completion: nil)
}

я смог добиться этого с помощью следующего подхода (это работает для нижнего колонтитула таким же образом).

во-первых, вам понадобится небольшая

мой AutoLayout работает очень хорошо:

CGSize headerSize = [headerView systemLayoutSizeFittingSize:CGSizeMake(CGRectGetWidth([UIScreen mainScreen].bounds), 0) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
headerView.frame = CGRectMake(0, 0, headerSize.width, headerSize.height);
self.tableView.tableHeaderView = headerView;

вот как вы можете сделать в вашем UIViewController

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if headerView.frame.size.height == 0 {
      headerView.label.preferredMaxLayoutWidth = view.bounds.size.width - 20
      let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height

      headerView.frame.size = CGSize(width: tableView.bounds.size.width, height: height)
    }
  }

любое ограничение на основе UIView может быть хорошим tableHeaderView.

нужно установить tableFooterView перед тем, как наложить дополнительное ограничение трейлинга на tableFooterView и tableHeaderView.

- (void)viewDidLoad {

    ........................
    // let self.headerView is some constraint-based UIView
    self.tableView.tableFooterView = [UIView new];
    [self.headerView layoutIfNeeded];
    self.tableView.tableHeaderView = self.headerView;

    [self.tableView.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor].active = YES;
    [self.tableView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
    [self.tableView.topAnchor constraintEqualToAnchor:self.headerView.topAnchor].active = YES;
    [self.tableFooterView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;

}

можно найти все детали и фрагменты кода здесь

Я придумал обходной путь. оберните ваши автомакет wrriten вид заголовка xib в пустой фантик наследник UIView, и назначить вид заголовка в собственность tableViewHeader TableView для это.

    UIView *headerWrapper = [[UIView alloc] init];
    AXLHomeDriverHeaderView *headerView = [AXLHomeDriverHeaderView loadViewFromNib];
    [headerWrapper addSubview:headerView];
    [headerView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(headerWrapper);
    }];
    self.tableView.tableHeaderView = headerView;

принятый ответ полезен только для таблиц с одним разделом. Для многосекционных UITableView просто убедитесь, что ваш заголовок наследует от UITableViewHeaderFooterView и вы будете в порядке.

в качестве альтернативы, просто вставьте свой текущий заголовок в contentView на UITableViewHeaderFooterView. Точно как UITableViewCell строительство.

для пользователей Xamarin:

public override void ViewDidLayoutSubviews()
{
    base.ViewDidLayoutSubviews();

    TableviewHeader.SetNeedsLayout();
    TableviewHeader.LayoutIfNeeded();

    var height = TableviewHeader.SystemLayoutSizeFittingSize(UIView.UILayoutFittingCompressedSize).Height;
    var frame = TableviewHeader.Frame;
    frame.Height = height;
    TableviewHeader.Frame = frame;
}

предполагая, что вы назвали представление заголовка вашего tableview как TableviewHeader