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

86

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

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

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

Поскольку overviewLabel (например, текст «Список обзорной информации здесь.») Имеет динамическое содержимое, я использую автоматическое размещение, чтобы изменить его размер и его супервизор. У меня все красиво меняется, за исключением tableHeaderView, который находится прямо под представлением таблицы Paralax в иерархии.

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

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

В этом случае headerFrameHeight - это ручной расчет высоты tableViewHeader следующим образом (innerHeaderView - это белая область или второе «представление», 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: Как отмечали другие, решение, опубликованное в Edit 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;
}
Эндрю Кросс
источник
2
setTableHeaderViewне работает на Xcode6. Проблема в том, что ячейки перекрываются tableHeaderView. Однако он работает на Xcode5
DawnSong
@DawnSong знает решение для Xcode6, чтобы я мог обновить ответ?
Эндрю Кросс,
1
После длительного пробного использования я использую UITableViewCell вместо tableHeaderView, и он работает.
DawnSong
Я тоже голову разбил!
Акшит Завери
Настоящее полное решение для автоматической
раскладки

Ответы:

35

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

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

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

TomSwift
источник
Он возвращается с height = 0, хотя я не уверен на 100%, что правильно настроил его, поскольку это не UITableViewCell, поэтому нет contentView для вызова "systemLayoutSizeFittingSize". Что я пробовал: [self.headerView setNeedsLayout]; [self.headerView layoutIfNeeded]; CGFloat height = [self.headerView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize] .height; CGRect headerFrame = self.headerView.frame; headerFrame.size.height = высота; self.headerView.frame = headerFrame; [self.listTableView setTableHeaderView: self.headerView]; Я также подтвердил, что предпочтительный Макс ... действителен.
Эндрю Кросс
Ага! Я понял, что мне нужно было сделать [self.innerHeaderView systemLayoutSizeFittingSize ...] вместо self.headerView, поскольку это то, что имеет фактическое динамическое содержимое. Благодаря тонну!
Эндрю Кросс
Это работает, но в моем приложении есть небольшое несоответствие по высоте. Вероятно, потому что на моих этикетках используется текст с атрибутами и динамический тип.
биография,
23

Я действительно боролся с этим, и установка настройки в 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
        }
    }
Даниэль Галаско
источник
для обновленного решения с минимальным кодом попробуйте следующее: stackoverflow.com/a/63594053/3933302
Сураб Шарма
23

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

Просто добавьте это в свой 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

п-солнце
источник
Привет, п-солнце. Спасибо за отличный пример. Кажется, у меня проблема с tableViewHeader. Я пытался использовать те же ограничения, что и в вашем примере, но не работал. Как именно я должен добавить ограничения к метке, для которой я хочу увеличить высоту? Спасибо за любое предложение
Боннке
1
Убедитесь, что вы прикрепили ярлык, который хотите сделать выше @IBOutlet makeThisTallerи @IBAction fun makeThisTallerпохожий на этот пример. Кроме того, ограничьте все стороны вашей метки tableViewHeader (например, сверху, снизу, слева и справа).
п-вс,
Большое спасибо! Наконец решено добавлением этой строки: lblFeedDescription.preferredMaxLayoutWidth = lblFeedDescription.bounds.widthгде label - это тот, размер которого я хочу увеличить. Благодарность !
Боннке,
Благодарность! Если вы делаете это внутри View вместо ViewController, то вместо переопределения viewDidLayoutSubviews вы можете переопределить layoutSubviews
Энди Вайнштейн
4

Это решение изменяет размер 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
        }
    }
}

См. Также этот пост: https://stackoverflow.com/a/34689293/1245231

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

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

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

self.listTableView.tableHeaderView = nil;
[self.headerView removeFromSuperview];
Ким Андре Санд
источник
2

Это сработало для меня на 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

}
Рави Рандерия
источник
2

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

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
    }
}
Сай Кумар Редди
источник
1

Для 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];});
RunLoop
источник
0

В моем случае viewDidLayoutSubviewsсработало лучше. viewWillLayoutSubviewsвызывает появление белых линий a 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];
    }
}
Денис Кутлубаев
источник
0

Вполне возможно использовать общий AutoLayout на основе UIViewлюбой внутренней структуры subview 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
    }
}
Malex
источник
0

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

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

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

мяу2x
источник
0

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

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
        }
    }
}
Сураб Шарма
источник