Como defino a altura de tableHeaderView (UITableView) com autolayout?

86

Estou batendo minha cabeça contra a parede com isso nas últimas 3 ou 4 horas e não consigo descobrir. Eu tenho um UIViewController com um UITableView de tela inteira dentro dele (há algumas outras coisas na tela, é por isso que não posso usar um UITableViewController) e quero que meu tableHeaderView redimensione com autolayout. Desnecessário dizer que não está cooperando.

Veja a imagem abaixo.

insira a descrição da imagem aqui

Como o overviewLabel (por exemplo, o texto "Listar informações gerais aqui.") Tem conteúdo dinâmico, estou usando o autolayout para redimensioná-lo e sua visualização. Eu tenho tudo redimensionado muito bem, exceto para o tableHeaderView, que está logo abaixo do Paralax Table View na pesquisa.

A única maneira que encontrei de redimensionar a visualização do cabeçalho é programaticamente, com o seguinte código:

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

Nesse caso, headerFrameHeight é um cálculo manual da altura de tableViewHeader da seguinte maneira (innerHeaderView é a área em branco ou a segunda "Visualização", 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);

O cálculo manual funciona, mas é desajeitado e sujeito a erros se as coisas mudarem no futuro. O que eu quero fazer é redimensionar automaticamente o tableHeaderView com base nas restrições fornecidas, como você faria em qualquer outro lugar. Mas pela minha vida, eu não consigo entender.

Existem vários posts no SO sobre isso, mas nenhum é claro e acabou me confundindo mais. Aqui estão alguns:

Realmente não faz sentido alterar a propriedade translatesAutoresizingMaskIntoConstraints para NO, já que isso apenas causa erros para mim e não faz sentido conceitualmente de qualquer maneira.

Qualquer ajuda seria realmente apreciada!

EDIT 1: Graças à sugestão de TomSwift, eu fui capaz de descobrir. Em vez de calcular manualmente a altura da visão geral, posso fazer com que ela seja calculada da seguinte maneira e, em seguida, reconfigure o tableHeaderView como antes.

[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];

Edição 2: como outros notaram, a solução postada na Edição 1 não parece funcionar em viewDidLoad. No entanto, parece funcionar em viewWillLayoutSubviews. Código de exemplo abaixo:

// 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;
}
Andrew Cross
fonte
2
setTableHeaderViewnão funciona no Xcode6. O problema é que as células são sobrepostas por tableHeaderView. No entanto, funciona no Xcode5
DawnSong
@DawnSong conhece uma solução para o Xcode6 para que eu possa atualizar a resposta?
Andrew Cross
1
Depois de muita experiência, uso UITableViewCell em vez de tableHeaderView e funciona.
DawnSong
Eu esmaguei minha cabeça também!
Akshit Zaveri,
A verdadeira solução autolayout completa está aqui
malex

Respostas:

35

Você precisa usar o UIView systemLayoutSizeFittingSize:método para obter o tamanho limite mínimo de sua visualização de cabeçalho.

Eu forneço uma discussão mais aprofundada sobre o uso desta API neste Q / A:

Como redimensionar superview para caber em todas as subviews com autolayout?

TomSwift
fonte
Ele está voltando com height = 0, embora eu não tenha 100% de certeza se configurei corretamente, pois não é um UITableViewCell, então não há o contentView para chamar "systemLayoutSizeFittingSize". O que experimentei: [self.headerView setNeedsLayout]; [self.headerView layoutIfNeeded]; CGFloat height = [self.headerView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize] .height; CGRect headerFrame = self.headerView.frame; headerFrame.size.height = height; self.headerView.frame = headerFrame; [self.listTableView setTableHeaderView: self.headerView]; Também confirmei que preferredMax ... é válido.
Andrew Cross
Aha! Descobri, eu precisava fazer [self.innerHeaderView systemLayoutSizeFittingSize ...] em vez de self.headerView, pois é isso que tem o conteúdo dinâmico real. Muito obrigado!
Andrew Cross
Funciona, mas no meu aplicativo há uma pequena discrepância de altura. Provavelmente porque minhas etiquetas estão usando um texto atribuído e um tipo dinâmico.
bio de
23

Eu realmente lutei com este e colocar a configuração em viewDidLoad não funcionou para mim, pois o quadro não está definido em viewDidLoad, também acabei com toneladas de avisos confusos onde a altura do layout automático encapsulado do cabeçalho estava sendo reduzida a 0 Eu só percebi o problema no iPad ao apresentar um tableView em uma apresentação de formulário.

O que resolveu o problema para mim foi definir o tableViewHeader em viewWillLayoutSubviews em vez de em 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
        }
    }
Daniel Galasko
fonte
para uma solução atualizada com código mínimo, experimente isto: stackoverflow.com/a/63594053/3933302
Sourabh Sharma
23

Eu descobri uma maneira elegante de usar o layout automático para redimensionar os cabeçalhos das tabelas, com e sem animação.

Basta adicionar isso ao seu 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()
    }
}

Para redimensionar de acordo com um rótulo que muda dinamicamente:

@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)
}

Para animar um redimensionamento de acordo com as alterações em uma restrição:

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

Consulte o projeto AutoResizingHeader para ver isso em ação. https://github.com/p-sun/Swift2-iOS9-UI

AutoResizingHeader

p-sol
fonte
Ei p-sol. Obrigado pelo ótimo exemplo. Parece que tenho problemas com tableViewHeader. Tentei ter as mesmas restrições do seu exemplo, mas não funcionou. Como exatamente devo adicionar restrições ao rótulo que desejo aumentar a altura? Obrigado por qualquer sugestão
Bonnke,
1
Certifique-se de prender a etiqueta que deseja tornar mais alta @IBOutlet makeThisTallere @IBAction fun makeThisTallercomo no exemplo. Além disso, restrinja todos os lados de seu rótulo ao tableViewHeader (por exemplo, superior, inferior, esquerdo e direito).
p-dom
Muito obrigado! Resolvido finalmente adicionando esta linha: lblFeedDescription.preferredMaxLayoutWidth = lblFeedDescription.bounds.widthonde rótulo é aquele que desejo aumentar de tamanho. Obrigado !
Bonnke
Obrigado! Se você estiver fazendo isso dentro de um View em vez de um ViewController, em vez de substituir viewDidLayoutSubviews, você pode substituir layoutSubviews
Andy Weinstein
4

Esta solução redimensiona o tableHeaderView e evita o loop infinito no viewDidLayoutSubviews()método que estava usando com algumas das outras respostas aqui:

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
        }
    }
}

Veja também esta postagem: https://stackoverflow.com/a/34689293/1245231

Petrsyn
fonte
1
Também estou usando esse método. Acho que é o melhor porque não há como mexer nas restrições. Também é muito compacto.
bio de
3

Sua solução usando systemLayoutSizeFittingSize: funciona se a visualização do cabeçalho for atualizada apenas uma vez em cada aparência da visualização. No meu caso, a visualização do cabeçalho foi atualizada várias vezes para refletir as mudanças de status. Mas systemLayoutSizeFittingSize: sempre relatou o mesmo tamanho. Ou seja, o tamanho correspondente à primeira atualização.

Para obter systemLayoutSizeFittingSize: para retornar o tamanho correto após cada atualização, eu tive que primeiro remover a visualização do cabeçalho da tabela antes de atualizá-la e adicioná-la novamente:

self.listTableView.tableHeaderView = nil;
[self.headerView removeFromSuperview];
Kim André Sand
fonte
2

Isso funcionou para mim no ios10 e 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

}
Ravi Randeria
fonte
2

Funciona tanto na visualização do cabeçalho quanto no rodapé, basta substituir o cabeçalho pelo rodapé

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
    }
}
Sai kumar Reddy
fonte
1

Para iOS 12 e superior, as etapas a seguir garantirão que o autolayout funcione corretamente na tabela e no cabeçalho.

  1. Crie primeiro sua tableView e, em seguida, o cabeçalho.
  2. No final do código de criação de seu cabeçalho, chame:
[headerV setNeedsLayout];
[headerV layoutIfNeeded];
  1. Após a mudança de orientação, marque o cabeçalho do layout novamente e recarregue a tabela. Isso precisa acontecer um pouco após a mudança de orientação ser relatada:
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
fonte
0

No meu caso viewDidLayoutSubviewsfuncionou melhor. viewWillLayoutSubviewsfaz com que as linhas brancas de tableViewa apareçam. Também adicionei a verificação se meu headerViewobjeto já existe.

- (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];
    }
}
Denis Kutlubaev
fonte
0

É bem possível usar AutoLayout genérico com base UIViewem qualquer estrutura de subvisualização interna do AL como um tableHeaderView.

A única coisa que se precisa é definir um tableFooterViewantes simples !

Vamos self.headerViewalguns baseados em restrições 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;
}

Se a self.headerViewaltura for alterada sob a rotação da IU, será necessário implementar

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

Pode-se usar a categoria ObjC para esta finalidade

@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

E também a extensão 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
fonte
0

Esta solução funciona perfeitamente para mim:

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

Ele estende o UITableViewController. Mas se você estiver usando apenas um UITableView, ele ainda funcionará, apenas estenda o UITableView ao invés do UITableViewController. Chame os métodos sizeHeaderToFit()ou sizeFooterToFit()sempre que houver um evento que altere a tableViewHeaderaltura.

meow2x
fonte
0

Copiado desta postagem

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
        }
    }
}
Sourabh Sharma
fonte