Использование autolayout в tableHeaderView

У меня есть UIView подкласс, который содержит многострочный UILabel, Это представление использует autolayout.

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

Я хотел бы установить эту точку зрения как tableHeaderView из UITableView (не заголовок раздела). Высота этого заголовка будет зависеть от текста метки, который, в свою очередь, зависит от ширины устройства. Разновидность сценария autolayout должна быть хороша в.

Я нашел и попытался найти множество решений, чтобы это работало, но безрезультатно. Некоторые из вещей, которые я пробовал:

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

Некоторые из различных сбоев, с которыми я столкнулся:

  • метка выходит за пределы ширины вида, не переносится
  • высота рамки 0
  • приложение вылетает за исключением Auto Layout still required after executing -layoutSubviews

Решение (или решения, если необходимо) должно работать как для iOS 7, так и для iOS 8. Обратите внимание, что все это делается программно. Я создал небольшой пример проекта на случай, если вы захотите взломать его, чтобы увидеть проблему. Я сбросил свои усилия до следующей начальной точки:

SCAMessageView *header = [[SCAMessageView alloc] init];
header.titleLabel.text = @"Warning";
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.";
self.tableView.tableHeaderView = header;

Что мне не хватает?

9 ответов

Решение

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

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

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

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

tableView.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
var frame = header.frame
frame.size.height = height
header.frame = frame
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()
        let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
        var frame = header.frame
        frame.size.height = height
        header.frame = frame
        self.tableHeaderView = header
    }
}

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

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

Для тех, кто все еще ищет решение, это для Swift 3 и iOS 9+. Вот один, использующий только AutoLayout. Также корректно обновляется при повороте устройства.

extension UITableView {
    // 1.
    func setTableHeaderView(headerView: UIView) {
        headerView.translatesAutoresizingMaskIntoConstraints = false

        self.tableHeaderView = headerView

        // ** Must setup AutoLayout after set tableHeaderView.
        headerView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
        headerView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
        headerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
    }

    // 2.
    func shouldUpdateHeaderViewFrame() -> Bool {
        guard let headerView = self.tableHeaderView else { return false }
        let oldSize = headerView.bounds.size        
        // Update the size
        headerView.layoutIfNeeded()
        let newSize = headerView.bounds.size
        return oldSize != newSize
    }
}

Использовать:

override func viewDidLoad() {
    ...

    // 1.
    self.tableView.setTableHeaderView(headerView: customView)
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // 2. Reflect the latest size in tableHeaderView
    if self.tableView.shouldUpdateHeaderViewFrame() {

        // **This is where table view's content (tableHeaderView, section headers, cells) 
        // frames are updated to account for the new table header size.
        self.tableView.beginUpdates()
        self.tableView.endUpdates()
    }
}

Суть в том, что вы должны позволить tableView управлять кадром tableHeaderView так же, как ячейки табличного представления. Это делается через tableView"s beginUpdates/endUpdates,

Дело в том, что tableView не заботится об AutoLayout, когда он обновляет дочерние кадры. Использует текущий tableHeaderViewРазмер, чтобы определить, где должен быть первый заголовок ячейки / раздела.

1) Добавьте ограничение ширины, чтобы tableHeaderView использует эту ширину всякий раз, когда мы вызываем layoutIfNeeded(). Также добавьте centerX и верхние ограничения, чтобы правильно расположить его относительно tableView,

2) чтобы позволить tableView знает о последнем размере tableHeaderViewНапример, когда устройство поворачивается, в viewDidLayoutSubviews мы можем вызвать layoutIfNeeded () на tableHeaderView, Затем, если размер изменяется, вызовите beginUpdates / endUpdates.

Обратите внимание, что я не включаю beginUpdates / endUpdates в одну функцию, так как мы можем отложить вызов на более поздний.

Проверьте пример проекта

Следующие UITableView расширение решает все распространенные проблемы автопрокладки и позиционирования tableHeaderView без использования фреймов:

@implementation UITableView (AMHeaderView)

- (void)am_insertHeaderView:(UIView *)headerView
{
    self.tableHeaderView = headerView;

    NSLayoutConstraint *constraint = 
    [NSLayoutConstraint constraintWithItem: headerView
                                 attribute: NSLayoutAttributeWidth
                                 relatedBy: NSLayoutRelationEqual
                                    toItem: headerView.superview
                                 attribute: NSLayoutAttributeWidth
                                multiplier: 1.0
                                  constant: 0.0];
    [headerView.superview addConstraint:constraint];    
    [headerView layoutIfNeeded];

    NSArray *constraints = headerView.constraints;
    [headerView removeConstraints:constraints];

    UIView *layoutView = [UIView new];
    layoutView.translatesAutoresizingMaskIntoConstraints = NO;
    [headerView insertSubview:layoutView atIndex:0];

    [headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];
    [headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];

    [headerView addConstraints:constraints];

    self.tableHeaderView = headerView;
    [headerView layoutIfNeeded];
}

@end

Объяснение "странных" шагов:

  1. Сначала мы привязываем ширину headerView к ширине tableView: это помогает при поворотах и ​​предотвращает глубокий сдвиг влево X-центрированных подпредставлений headerView.

  2. (Волшебство!) Мы вставляем поддельный layoutView в headerView: в этот момент нам СТРОГО нужно удалить все ограничения headerView, расширить layoutView до headerView и затем восстановить начальные ограничения headerView. Бывает, что порядок ограничений имеет какой-то смысл! В способе мы получаем правильный автоматический расчет высоты headerView, а также правильный
    X-централизация для всех подвидов headerView.

  3. Тогда нам нужно только изменить макет headerView снова, чтобы получить правильный tableView
    вычисление высоты и расположение заголовка в виде над секциями без пересечения.

PS Работает и для iOS8. Здесь невозможно закомментировать любую строку кода в общем случае.

Некоторые ответы здесь помогли мне приблизиться к тому, что мне было нужно. Но я столкнулся с конфликтами с ограничением "UIView-Encapsulated-Layout-Width", которое устанавливается системой, при повороте устройства назад и вперед между портретным и альбомным режимом. Мое решение, приведенное ниже, в значительной степени основано на этой сущности по маркорументу (кредит ему): https://gist.github.com/marcoarment/1105553afba6b4900c10. Решение не опирается на представление заголовка, содержащее UILabel. Есть 3 части:

  1. Функция, определенная в расширении UITableView.
  2. Вызовите функцию из viewWillAppear() контроллера представления.
  3. Вызовите функцию из viewWillTransition() контроллера представления для обработки вращения устройства.

Расширение UITableView

func rr_layoutTableHeaderView(width:CGFloat) {
    // remove headerView from tableHeaderView:
    guard let headerView = self.tableHeaderView else { return }
    headerView.removeFromSuperview()
    self.tableHeaderView = nil

    // create new superview for headerView (so that autolayout can work):
    let temporaryContainer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
    temporaryContainer.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(temporaryContainer)
    temporaryContainer.addSubview(headerView)

    // set width constraint on the headerView and calculate the right size (in particular the height):
    headerView.translatesAutoresizingMaskIntoConstraints = false
    let temporaryWidthConstraint = NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: width)
    temporaryWidthConstraint.priority = 999     // necessary to avoid conflict with "UIView-Encapsulated-Layout-Width"
    headerView.addConstraint(temporaryWidthConstraint)
    headerView.frame.size = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)

    // remove the temporary constraint:
    headerView.removeConstraint(temporaryWidthConstraint)
    headerView.translatesAutoresizingMaskIntoConstraints = true

    // put the headerView back into the tableHeaderView:
    headerView.removeFromSuperview()
    temporaryContainer.removeFromSuperview()
    self.tableHeaderView = headerView
}

Используйте в UITableViewController

override func viewDidLoad() {
    super.viewDidLoad()

    // build the header view using autolayout:
    let button = UIButton()
    let label = UILabel()
    button.setTitle("Tap here", for: .normal)
    label.text = "The text in this header will span multiple lines if necessary"
    label.numberOfLines = 0
    let headerView = UIStackView(arrangedSubviews: [button, label])
    headerView.axis = .horizontal
    // assign the header view:
    self.tableView.tableHeaderView = headerView

    // continue with other things...
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.tableView.rr_layoutTableHeaderView(width: view.frame.width)
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    self.tableView.rr_layoutTableHeaderView(width: size.width)
}

Это должно сделать трюк для headerView или footerView для UITableView с использованием AutoLayout.

extension UITableView {

  var tableHeaderViewWithAutolayout: UIView? {
    set (view) {
      tableHeaderView = view
      if let view = view {
        lowerPriorities(view)
        view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
        tableHeaderView = view
      }
    }
    get {
      return tableHeaderView
    }
  }

  var tableFooterViewWithAutolayout: UIView? {
    set (view) {
      tableFooterView = view
      if let view = view {
        lowerPriorities(view)
        view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
        tableFooterView = view
      }
    }
    get {
      return tableFooterView
    }
  }

  fileprivate func lowerPriorities(_ view: UIView) {
    for cons in view.constraints {
      if cons.priority.rawValue == 1000 {
        cons.priority = UILayoutPriority(rawValue: 999)
      }
      for v in view.subviews {
        lowerPriorities(v)
      }
    }
  }
}

Это действительно так просто:

Преамбула. Быть в курсе, чтоине имеют абсолютно никакого отношения к нижним колонтитулам /заголовкам разделов в представлении таблицы. Они никоим образом не похожи, не связаны и не связаны .

Более того, в UIKit парадигма, обработка и обработка цикла представления украшений «таблицы» и «раздела» совершенно, совершенно различны, и, следовательно, вся проблема совершенно запутанна. Добавьте к этому совершенно странную проблему, описанную здесь, в пункте «2» (которая совершенно не похожа на любую другую обработку ограничений во всем UIKit), и можно увидеть, как возникла массовая путаница в отношении нижних колонтитулов таблиц.

Самый главный секрет заключается в следующем:

1. Ваш xib ДОЛЖЕН ИМЕТЬ <1000 вертикальный приоритет.

В вашем файле xib ваша вертикальная цепочка ограничений ДОЛЖНА быть меньше 1000. Вот и все, что нужно. Загрузите файл xib обычным способом.

      class DemoFooter: UIIView {
    class func build() -> UIView {
        return UINib(nibName: "DemoFooter", bundle: nil)
         .instantiate(withOwner: nil, options: nil)[0] as! UIView
    }
}

Это ошеломляющий «секрет» работы верхних и нижних колонтитулов таблиц.

2. Из-за проблемы Apple вы ДОЛЖНЫ «сбросить» tableFooterView. Это просто так. Совершенно бессмысленно пытаться обойти эту проблему Apple.

Вы ожидаете, что код будет выглядеть так

      tableFooterView.frame = .. the size

Простой факт: вы не можете этого сделать, вы должны сделать ЭТО:

      temp = tableFooterView
temp.frame = .. the size
tableFooterView = temp

Совершенно бессмысленно обсуждать, является ли это ошибкой Apple, программной ошибкой, глупостью, странным поведением, ошибкой или чем-то еще. Дело в том, что вам нужно «сбросить» tableFooterView.

(В вашем коде просто попробуйте по-другому, и вы увидите, что сначала нижний колонтитул просто «плавает в середине экрана». Абсолютно не имеет значения, где вы попробуете это сделать в цикле просмотра, потратите время. Пробуйте, как хотите! Это просто типичный провал UIKit.)

3. Обратите внимание на неправильный пример кода цикла просмотра, который можно найти в Интернете.

      override func viewDidLoad() {
    
    super.viewDidLoad()
    tableView.tableFooterView = DemoFooter.build()
}

override func viewWillLayoutSubviews() {
    
    if let f = tableView.tableFooterView {
        f.frame.size = f.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        tableView.tableFooterView = f
        print("Recall, you MUST HAVE <1000 priority vert in your xib.")
    }
    
    super.viewWillLayoutSubviews()
}

Ты делаешь это в не позже в цикле.

Вы НЕ «проверяете, изменилась ли высота», как это видно во многих примерах в сети. Очевидно, что это невероятно бессмысленная/очень плохая практика по причинам, на которые не стоит вдаваться здесь.

Для ясности: если это тривиальное статическое представление, вы можете просто сделать это, чтобы не вводить «viewDidLoad»:

      override func viewWillLayoutSubviews() {
    
    let f = QuickFoot.build()
    f.frame.size = f.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
    tableView.tableFooterView = f
    
    super.viewWillLayoutSubviews()
}

4. Перепроверьте точку (1).

Или просто ничего не будет работать (и вы будете получать спам с ошибками в консоли).

Использование расширения в Swift 3.0

extension UITableView {

    func setTableHeaderView(headerView: UIView?) {
        // set the headerView
        tableHeaderView = headerView

        // check if the passed view is nil
        guard let headerView = headerView else { return }

        // check if the tableHeaderView superview view is nil just to avoid
        // to use the force unwrapping later. In case it fail something really
        // wrong happened
        guard let tableHeaderViewSuperview = tableHeaderView?.superview else {
            assertionFailure("This should not be reached!")
            return
        }

        // force updated layout
        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()

        // set tableHeaderView width
        tableHeaderViewSuperview.addConstraint(headerView.widthAnchor.constraint(equalTo: tableHeaderViewSuperview.widthAnchor, multiplier: 1.0))

        // set tableHeaderView height
        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        tableHeaderViewSuperview.addConstraint(headerView.heightAnchor.constraint(equalToConstant: height))
    }

    func setTableFooterView(footerView: UIView?) {
        // set the footerView
        tableFooterView = footerView

        // check if the passed view is nil
        guard let footerView = footerView else { return }

        // check if the tableFooterView superview view is nil just to avoid
        // to use the force unwrapping later. In case it fail something really
        // wrong happened
        guard let tableFooterViewSuperview = tableFooterView?.superview else {
            assertionFailure("This should not be reached!")
            return
        }

        // force updated layout
        footerView.setNeedsLayout()
        footerView.layoutIfNeeded()

        // set tableFooterView width
        tableFooterViewSuperview.addConstraint(footerView.widthAnchor.constraint(equalTo: tableFooterViewSuperview.widthAnchor, multiplier: 1.0))

        // set tableFooterView height
        let height = footerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        tableFooterViewSuperview.addConstraint(footerView.heightAnchor.constraint(equalToConstant: height))
    }
}

Твои ограничения были совсем немного. Посмотрите на это и дайте мне знать, если у вас есть какие-либо вопросы. По какой-то причине у меня были трудности с получением фона вида, чтобы остаться красным? Поэтому я создал представление заполнителя, которое заполняет пробел, созданный titleLabel а также subtitleLabel высота, которая больше, чем высота imageView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        self.backgroundColor = [UIColor redColor];

        self.imageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"Exclamation"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
        self.imageView.tintColor = [UIColor whiteColor];
        self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
        self.imageView.backgroundColor = [UIColor redColor];
        [self addSubview:self.imageView];
        [self.imageView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self);
            make.width.height.equalTo(@40);
            make.top.equalTo(self).offset(0);
        }];

        self.titleLabel = [[UILabel alloc] init];
        self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
        self.titleLabel.font = [UIFont systemFontOfSize:14];
        self.titleLabel.textColor = [UIColor whiteColor];
        self.titleLabel.backgroundColor = [UIColor redColor];
        [self addSubview:self.titleLabel];
        [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self).offset(0);
            make.left.equalTo(self.imageView.mas_right).offset(0);
            make.right.equalTo(self).offset(-10);
            make.height.equalTo(@15);
        }];

        self.subtitleLabel = [[UILabel alloc] init];
        self.subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
        self.subtitleLabel.font = [UIFont systemFontOfSize:13];
        self.subtitleLabel.textColor = [UIColor whiteColor];
        self.subtitleLabel.numberOfLines = 0;
        self.subtitleLabel.backgroundColor = [UIColor redColor];
        [self addSubview:self.subtitleLabel];
        [self.subtitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.titleLabel.mas_bottom);
            make.left.equalTo(self.imageView.mas_right);
            make.right.equalTo(self).offset(-10);
        }];

        UIView *fillerView = [[UIView alloc] init];
        fillerView.backgroundColor = [UIColor redColor];
        [self addSubview:fillerView];
        [fillerView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.imageView.mas_bottom);
            make.bottom.equalTo(self.subtitleLabel.mas_bottom);
            make.left.equalTo(self);
            make.right.equalTo(self.subtitleLabel.mas_left);
        }];
    }

    return self;
}

Я добавлю свои 2 цента, так как этот вопрос высоко проиндексирован в Google. Я думаю, что вы должны использовать

self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension
self.tableView.estimatedSectionHeaderHeight = 200 //a rough estimate, doesn't need to be accurate

в вашем ViewDidLoad, Также для загрузки кастома UIView к Header вы действительно должны использовать viewForHeaderInSection метод делегата. Вы можете иметь кастом Nib файл для вашего заголовка (UIView СИБ). Тот Nib должен иметь класс контроллера, который подклассы UITableViewHeaderFooterView лайк-

class YourCustomHeader: UITableViewHeaderFooterView {
    //@IBOutlets, delegation and other methods as per your needs
}

Убедитесь, что имя вашего Nib-файла совпадает с именем класса, чтобы не запутаться и им было легче управлять. лайк YourCustomHeader.xib а также YourCustomHeader.swift (содержащий class YourCustomHeader). Затем просто назначьте YourCustomHeader в ваш Nib-файл с помощью инспектора идентификации в конструкторе интерфейсов.

Затем зарегистрируйте Nib файл в качестве заголовка в главном контроллере вида viewDidLoad лайк-

tableView.register(UINib(nibName: "YourCustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "YourCustomHeader")

И тогда в вашем heightForHeaderInSection просто вернись UITableViewAutomaticDimension, Вот так должны выглядеть делегаты:

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
     let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "YourCustomHeader") as! YourCustomHeader
     return headerView
}

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
     return UITableViewAutomaticDimension
}

Это гораздо более простой и подходящий способ обойтись без "хакерских" способов, предложенных в принятом ответе, поскольку множественные принудительные макеты могут повлиять на производительность вашего приложения, особенно если у вас есть несколько пользовательских заголовков в табличном представлении. Как только вы сделаете вышеуказанный метод, как я предлагаю, вы заметите Header (и или Footer) представление расширяется и сокращается волшебным образом в зависимости от размера содержимого пользовательского представления (при условии, что вы используете AutoLayout в настраиваемом представлении, т.е. YourCustomHeaderНиб файл).

Другие вопросы по тегам