Как установить высоту tableHeaderView (UITableView) с помощью автоматического размещения?

Я бьюсь головой об стену с этим последние 3 или 4 часа, и я не могу понять это. У меня есть UIViewController с полноэкранным UITableView внутри (есть некоторые другие вещи на экране, поэтому я не могу использовать UITableViewController), и я хочу получить свой tableHeaderView для изменения размера с autolayout. Излишне говорить, что это не сотрудничество.

Смотрите скриншот ниже.

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

Так как OverviewLabel (например, текст "Список обзорной информации здесь.") Имеет динамическое содержимое, я использую autolayout, чтобы изменить его размер, и это superview. У меня все отлично меняется, за исключением tableHeaderView, который находится прямо под Paralax Table View в hiearchy.

Я нашел единственный способ изменить размер этого заголовка программно со следующим кодом:

CGRect headerFrame = self.headerView.frame;
headerFrame.size.height = headerFrameHeight;
self.headerView.frame = headerFrame;
[self.listTableView setTableHeaderView:self.headerView];

В этом случае headerFrameHeight представляет собой ручной расчет высоты tableViewHeader следующим образом (innerHeaderView - это белая область, или второй "View", headerView - это tableHeaderView):

CGFloat startingY = self.innerHeaderView.frame.origin.y + self.overviewLabel.frame.origin.y;
CGRect overviewSize = [self.overviewLabel.text
                       boundingRectWithSize:CGSizeMake(290.f, CGFLOAT_MAX)
                       options:NSStringDrawingUsesLineFragmentOrigin
                       attributes:@{NSFontAttributeName: self.overviewLabel.font}
                       context:nil];
CGFloat overviewHeight = overviewSize.size.height;
CGFloat overviewPadding = ([self.overviewLabel.text length] > 0) ? 10 : 0; // If there's no overviewText, eliminate the padding in the overall height.
CGFloat headerFrameHeight = ceilf(startingY + overviewHeight + overviewPadding + 21.f + 10.f);

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

На SO есть несколько постов по этому поводу, но ни один из них не ясен и закончил тем, что запутал меня больше Вот несколько из них:

На самом деле не имеет смысла менять свойство translatesAutoresizingMaskIntoConstraints на NO, так как это просто вызывает для меня ошибки и в любом случае не имеет смысла концептуально.

Любая помощь будет принята с благодарностью!

РЕДАКТИРОВАТЬ 1: Благодаря предложению TomSwift, я смог понять это. Вместо того, чтобы вручную вычислять высоту обзора, я могу рассчитать ее для меня следующим образом, а затем заново установить tableHeaderView, как и раньше.

[self.headerView setNeedsLayout];
[self.headerView layoutIfNeeded];
CGFloat height = [self.innerHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + self.innerHeaderView.frame.origin.y; // adding the origin because innerHeaderView starts partway down headerView.

CGRect headerFrame = self.headerView.frame;
headerFrame.size.height = height;
self.headerView.frame = headerFrame;
[self.listTableView setTableHeaderView:self.headerView];

Редактировать 2: Как уже отмечалось, решение, опубликованное в Редакторе 1, похоже, не работает в viewDidLoad. Однако, похоже, что он работает в viewWillLayoutSubviews. Пример кода ниже:

// Note 1: The variable names below don't match the variables above - this is intended to be a simplified "final" answer.
// Note 2: _headerView was previously assigned to tableViewHeader (in loadView in my case since I now do everything programatically).
// Note 3: autoLayout code can be setup programatically in updateViewConstraints.
- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    [_headerWrapper setNeedsLayout];
    [_headerWrapper layoutIfNeeded];
    CGFloat height = [_headerWrapper systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    CGRect headerFrame = _headerWrapper.frame;
    headerFrame.size.height = height;
    _headerWrapper.frame = headerFrame;
    _tableView.tableHeaderView = _headerWrapper;
}

9 ответов

Решение

Вам нужно использовать UIView systemLayoutSizeFittingSize: метод, чтобы получить минимальный размер ограничения вашего представления заголовка.

Я предоставляю дальнейшее обсуждение использования этого API в этом Q/A:

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

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

Просто добавьте это в свой View Controller.

func sizeHeaderToFit(tableView: UITableView) {
    if let headerView = tableView.tableHeaderView {
        let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
        var frame = headerView.frame
        frame.size.height = height
        headerView.frame = frame
        tableView.tableHeaderView = headerView
        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()
    }
}

Чтобы изменить размер в соответствии с динамически меняющейся меткой:

@IBAction func addMoreText(sender: AnyObject) {
    self.label.text = self.label.text! + "\nThis header can dynamically resize according to its contents."
}

override func viewDidLayoutSubviews() {
    // viewDidLayoutSubviews is called when labels change.
    super.viewDidLayoutSubviews()
    sizeHeaderToFit(tableView)
}

Чтобы анимировать изменение размера в соответствии с изменениями в ограничении:

@IBOutlet weak var makeThisTallerHeight: NSLayoutConstraint!

@IBAction func makeThisTaller(sender: AnyObject) {

    UIView.animateWithDuration(0.3) {
        self.tableView.beginUpdates()
        self.makeThisTallerHeight.constant += 20
        self.sizeHeaderToFit(self.tableView)
        self.tableView.endUpdates()
    }
}

Посмотрите проект AutoResizingHeader, чтобы увидеть это в действии. https://github.com/p-sun/Swift2-iOS9-UI

AutoResizingHeader

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

Что решило проблему для меня, так это установив tableViewHeader в viewWillLayoutSubviews, а не в viewDidLoad.

func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        if tableView.tableViewHeaderView == nil {
            let header: MyHeaderView = MyHeaderView.createHeaderView()
            header.setNeedsUpdateConstraints()
            header.updateConstraintsIfNeeded()
            header.frame = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), CGFloat.max)
            var newFrame = header.frame
            header.setNeedsLayout()
            header.layoutIfNeeded()
            let newSize = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
            newFrame.size.height = newSize.height
            header.frame = newFrame
            self.tableView.tableHeaderView = header
        }
    }

Это решение изменяет размер tableHeaderView и позволяет избежать бесконечного цикла в viewDidLayoutSubviews() метод, который я имел с некоторыми другими ответами здесь:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let headerView = tableView.tableHeaderView {
        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        var headerFrame = headerView.frame

        // comparison necessary to avoid infinite loop
        if height != headerFrame.size.height {
            headerFrame.size.height = height
            headerView.frame = headerFrame
            tableView.tableHeaderView = headerView
        }
    }
}

Смотрите также этот пост: /questions/37824455/ustanovka-tableheaderview-vyisotyi-dinamicheski/37824464#37824464

Ваше решение, использующее systemLayoutSizeFittingSize: работает, если представление заголовка просто обновляется один раз при каждом отображении представления. В моем случае вид заголовка обновлялся несколько раз, чтобы отражать изменения статуса. Но systemLayoutSizeFittingSize: всегда сообщает об одном и том же размере. То есть размер соответствует первому обновлению.

Чтобы получить systemLayoutSizeFittingSize: возвращать правильный размер после каждого обновления, мне пришлось сначала удалить представление заголовка таблицы перед обновлением и повторно добавить его:

self.listTableView.tableHeaderView = nil;
[self.headerView removeFromSuperview];

Это работало для меня на ios10 и Xcode 8

func layoutTableHeaderView() {

    guard let headerView = tableView.tableHeaderView else { return }
    headerView.translatesAutoresizingMaskIntoConstraints = false

    let headerWidth = headerView.bounds.size.width;
    let temporaryWidthConstraints = NSLayoutConstraint.constraintsWithVisualFormat("[headerView(width)]", options: NSLayoutFormatOptions(rawValue: UInt(0)), metrics: ["width": headerWidth], views: ["headerView": headerView])

    headerView.addConstraints(temporaryWidthConstraints)

    headerView.setNeedsLayout()
    headerView.layoutIfNeeded()

    let headerSize = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
    let height = headerSize.height
    var frame = headerView.frame

    frame.size.height = height
    headerView.frame = frame

    self.tableView.tableHeaderView = headerView

    headerView.removeConstraints(temporaryWidthConstraints)
    headerView.translatesAutoresizingMaskIntoConstraints = true

}

Он работает как для представления заголовка, так и для нижнего колонтитула, просто замените верхний колонтитул на нижний колонтитул

func sizeHeaderToFit() {
    if let headerView = tableView.tableHeaderView {

        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()

        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        var frame = headerView.frame
        frame.size.height = height
        headerView.frame = frame

        tableView.tableHeaderView = headerView
    }
}

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

  1. Сначала создайте свой tableView, затем заголовок.
  2. В конце кода создания заголовка вызовите:
[headerV setNeedsLayout];
[headerV layoutIfNeeded];
  1. После изменения ориентации снова отметьте заголовок для макета и перезагрузите таблицу, это должно произойти немного после сообщения об изменении ориентации:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 *NSEC_PER_SEC), dispatch_get_main_queue(), ^{

  [tableV.tableHeaderView setNeedsLayout];
  [tableV.tableHeaderView layoutIfNeeded];
  [tableV reloadData];});

Вполне возможно использовать общий AutoLayout на основе UIView с любой внутренней структурой подвидов AL как tableHeaderView,

Единственное, что нужно, это установить простую tableFooterView до!

Позволять self.headerView это некоторые на основе ограничений UIView,

- (void)viewDidLoad {

    ........................

    self.tableView.tableFooterView = [UIView new];

    [self.headerView layoutIfNeeded]; // to set initial size

    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;

    // and the key constraint
    [self.tableFooterView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
}

Если self.headerView изменения высоты при повороте пользовательского интерфейса нужно реализовать

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

    [coordinator animateAlongsideTransition: ^(id<UIViewControllerTransitionCoordinatorContext> context) {
        // needed to resize header height
        self.tableView.tableHeaderView = self.headerView;
    } completion: NULL];
}

Для этого можно использовать категорию ObjC

@interface UITableView (AMHeaderView)
- (void)am_insertHeaderView:(UIView *)headerView;
@end

@implementation UITableView (AMHeaderView)

- (void)am_insertHeaderView:(UIView *)headerView {

    NSAssert(self.tableFooterView, @"Need to define tableFooterView first!");

    [headerView layoutIfNeeded];

    self.tableHeaderView = headerView;

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

    [self.tableFooterView.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor].active = YES;
}
@end

А также расширение Swift

extension UITableView {

    func am_insertHeaderView2(_ headerView: UIView) {

        assert(tableFooterView != nil, "Need to define tableFooterView first!")

        headerView.layoutIfNeeded()

        tableHeaderView = headerView

        leadingAnchor.constraint(equalTo: headerView.leadingAnchor).isActive = true
        trailingAnchor.constraint(equalTo: headerView.trailingAnchor).isActive = true
        topAnchor.constraint(equalTo: headerView.topAnchor).isActive = true

        tableFooterView?.trailingAnchor.constraint(equalTo: headerView.trailingAnchor).isActive = true
    }
}

Это решение отлично работает для меня:

https://spin.atomicobject.com/2017/08/11/swift-extending-uitableviewcontroller/

Это расширяет UITableViewController. Но если вы просто используете UITableView, он все равно будет работать, просто расширьте UITableView вместо UITableViewController. Вызывать методы sizeHeaderToFit() или же sizeFooterToFit() всякий раз, когда есть событие, которое меняет tableViewHeader рост.

Скопировано из этого сообщения

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    if let headerView = tableView.tableHeaderView {

        let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        var headerFrame = headerView.frame
        
        //Comparison necessary to avoid infinite loop
        if height != headerFrame.size.height {
            headerFrame.size.height = height
            headerView.frame = headerFrame
            tableView.tableHeaderView = headerView
        }
    }
}

В моем случае viewDidLayoutSubviews работал лучше. viewWillLayoutSubviews вызывает белые линии tableView появляться. Также я добавил проверку, если мой headerView объект уже существует

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    if ( ! self.userHeaderView ) {
        // Setup HeaderView
        self.userHeaderView = [[[NSBundle mainBundle] loadNibNamed:@"SSUserHeaderView" owner:self options:nil] objectAtIndex:0];
        [self.userHeaderView setNeedsLayout];
        [self.userHeaderView layoutIfNeeded];
        CGFloat height = [self.userHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect headerFrame = self.userHeaderView.frame;
        headerFrame.size.height = height;
        self.userHeaderView.frame = headerFrame;
        self.tableView.tableHeaderView = self.userHeaderView;

        // Update HeaderView with data
        [self.userHeaderView updateWithProfileData];
    }
}
Другие вопросы по тегам