Использование автозапуска в таблицеHeaderView

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

enter image description here

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

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

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

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

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

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

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;

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

Ответы

Ответ 1

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

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

Ответ 2

Для тех, кто все еще ищет решение, это для 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 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 beginUpdates/endUpdates.

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

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

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

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

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

Ответ 3

Следующее расширение 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

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

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

  • (Magic!) Мы вставляем поддельный layoutView в headerView: В настоящий момент нам необходимо удалить все ограничения headerView, развернуть layoutView в headerView, а затем восстановить начальный заголовок ограничения. Бывает, что порядок ограничений имеет какой-то смысл! В способе мы получаем правильный автоматический подсчет высоты заголовка, а также правильный X-централизация для всех заголовков headerView.

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

P.S. Он также работает для iOS8. В общем случае невозможно прокомментировать любую кодовую строку.

Ответ 4

Использование расширения в 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))
    }
}

Ответ 5

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

  • Функция, определенная в расширении для UITableView.
  • Вызвать функцию из представления контроллера ViewWillAppear().
  • Вызовите функцию из вида 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)
}

Ответ 6

Я добавлю свои 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). То, что 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, файл nib).

Ответ 7

Ваши ограничения были совсем немного. Взгляните на это и сообщите мне, если у вас есть какие-либо вопросы. По какой-то причине мне было трудно получить фон обзора, чтобы он остался красным? Таким образом, я создал представление наполнителя, которое заполняет промежуток, созданный с высотой 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;
}