¿Cómo configuro la altura de tableHeaderView (UITableView) con autolayout?

86

He estado aplastando mi cabeza contra la pared con esto durante las últimas 3 o 4 horas y parece que no puedo entenderlo. Tengo un UIViewController con un UITableView de pantalla completa dentro (hay algunas otras cosas en la pantalla, por lo que no puedo usar un UITableViewController) y quiero que mi tableHeaderView cambie de tamaño con el diseño automático. No hace falta decir que no está cooperando.

Vea la captura de pantalla a continuación.

ingrese la descripción de la imagen aquí

Debido a que la etiqueta de descripción general (por ejemplo, el texto "Lista de información general aquí") tiene contenido dinámico, estoy usando el diseño automático para cambiar su tamaño y es una supervista. Tengo todo cambiando de tamaño muy bien, excepto por tableHeaderView, que está justo debajo de Paralax Table View en la jerarquía.

La única forma que he encontrado para cambiar el tamaño de esa vista de encabezado es programáticamente, con el siguiente código:

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

En este caso, headerFrameHeight es un cálculo manual de la altura tableViewHeader de la siguiente manera (innerHeaderView es el área blanca, o la segunda "Vista", headerView es 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);

El cálculo manual funciona, pero es torpe y propenso a errores si las cosas cambian en el futuro. Lo que quiero poder hacer es que el tableHeaderView cambie de tamaño automáticamente en función de las restricciones proporcionadas, como puede hacerlo en cualquier otro lugar. Pero por mi vida, no puedo entenderlo.

Hay varias publicaciones en SO sobre esto, pero ninguna es clara y terminó confundiéndome más. Aquí hay algunos:

Realmente no tiene sentido cambiar la propiedad translatesAutoresizingMaskIntoConstraints a NO, ya que eso solo me causa errores y no tiene sentido conceptualmente de todos modos.

¡Cualquier ayuda sería realmente apreciada!

EDICIÓN 1: Gracias a la sugerencia de TomSwift, pude resolverlo. En lugar de calcular manualmente la altura de la descripción general, puedo hacer que se calcule para mí de la siguiente manera y luego volver a configurar 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];

Edición 2: como han señalado otros, la solución publicada en la Edición 1 no parece funcionar en viewDidLoad. Sin embargo, parece funcionar en viewWillLayoutSubviews. Código de ejemplo a continuación:

// 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
fuente
2
setTableHeaderViewno funciona en Xcode6. El problema es que las celdas están superpuestas por tableHeaderView. Sin embargo, funciona en Xcode5
DawnSong
@DawnSong, ¿conoces una solución para Xcode6 para que pueda actualizar la respuesta?
Andrew Cross
1
Después de muchas pruebas, utilizo UITableViewCell en lugar de tableHeaderView y funciona.
DawnSong
¡Yo también me rompí la cabeza!
Akshit Zaveri
La verdadera solución completa de diseño automático está aquí
malex

Respuestas:

35

Debe utilizar el UIView systemLayoutSizeFittingSize:método para obtener el tamaño delimitador mínimo de su vista de encabezado.

Proporciono más información sobre el uso de esta API en esta Q / A:

¿Cómo cambiar el tamaño de la supervista para que se ajuste a todas las subvistas con diseño automático?

TomSwift
fuente
Vuelve con height = 0, aunque no estoy 100% seguro de haberlo configurado correctamente, ya que no es una UITableViewCell, por lo que no hay contentView para llamar "systemLayoutSizeFittingSize". Lo que intenté: [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]; También confirmé que favoriteMax ... es válido.
Andrew Cross
¡Ajá! Lo resolví, necesitaba hacer [self.innerHeaderView systemLayoutSizeFittingSize ...] en lugar de self.headerView ya que eso es lo que tiene el contenido dinámico real. ¡Gracias una tonelada!
Andrew Cross
Funciona, pero en mi aplicación hay una pequeña discrepancia de altura. Probablemente porque mis etiquetas usan un texto atribuido y un tipo dinámico.
biografía
23

Realmente luché con este y colocar la configuración en viewDidLoad no funcionó para mí ya que el marco no está configurado en viewDidLoad, también terminé con toneladas de advertencias desordenadas donde la altura de diseño automático encapsulado del encabezado se reducía a 0 Solo noté el problema en iPad al presentar un tableView en una presentación de formulario.

Lo que resolvió el problema para mí fue configurar tableViewHeader en viewWillLayoutSubviews en lugar de 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
fuente
para una solución actualizada con un código mínimo, pruebe esto: stackoverflow.com/a/63594053/3933302
Sourabh Sharma
23

Encontré una manera elegante de usar el diseño automático para cambiar el tamaño de los encabezados de las tablas, con y sin animación.

Simplemente agregue esto a su 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 cambiar el tamaño de acuerdo con una etiqueta que cambia dinámicamente:

@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 un cambio de tamaño de acuerdo con los cambios en una restricción:

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

Vea el proyecto AutoResizingHeader para ver esto en acción. https://github.com/p-sun/Swift2-iOS9-UI

AutoResizingHeader

p-sol
fuente
Oye p-sol. Gracias por un gran ejemplo. Parece que tengo un problema con tableViewHeader. Intenté tener las mismas restricciones que en su ejemplo, pero no funcionó. ¿Cómo exactamente debo agregar restricciones a la etiqueta que quiero aumentar la altura? Gracias por cualquier sugerencia
Bonnke
1
Asegúrese de enganchar la etiqueta a la que desea hacer más alto @IBOutlet makeThisTallery @IBAction fun makeThisTallercomo en el ejemplo. Además, restrinja todos los lados de su etiqueta al tableViewHeader (por ejemplo, arriba, abajo, izquierda y derecha).
p-dom
¡Muchas gracias! Finalmente resuelto agregando esta línea: lblFeedDescription.preferredMaxLayoutWidth = lblFeedDescription.bounds.widthdonde etiqueta es aquella que quiero aumentar de tamaño. Gracias !
Bonnke
¡Gracias! Si está haciendo esto dentro de una Vista en lugar de un ViewController, entonces en lugar de anular viewDidLayoutSubviews, puede anular layoutSubviews
Andy Weinstein
4

Esta solución cambia el tamaño de tableHeaderView y evita un bucle infinito en el viewDidLayoutSubviews()método que estaba teniendo con algunas de las otras respuestas aquí:

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

Vea también esta publicación: https://stackoverflow.com/a/34689293/1245231

petrsyn
fuente
1
Yo también estoy usando este método. Piense que es lo mejor porque no hay que jugar con las limitaciones. También es muy compacto.
biografía
3

Su solución usando systemLayoutSizeFittingSize: funciona si la vista de encabezado se actualiza una vez en cada apariencia de vista. En mi caso, la vista del encabezado se actualizó varias veces para reflejar los cambios de estado. Pero systemLayoutSizeFittingSize: siempre informó del mismo tamaño. Es decir, el tamaño correspondiente a la primera actualización.

Para obtener systemLayoutSizeFittingSize: para devolver el tamaño correcto después de cada actualización, primero tenía que eliminar la vista del encabezado de la tabla antes de actualizarla y volver a agregarla:

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

Esto funcionó para mí en ios10 y 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
fuente
2

Funciona tanto para la vista del encabezado como para el pie de página, simplemente reemplace el encabezado con el pie de página

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
fuente
1

Para iOS 12 y superior, los siguientes pasos garantizarán que el diseño automático funcione correctamente tanto en la tabla como en el encabezado.

  1. Primero crea tu tableView, luego el encabezado.
  2. Al final de su código de creación de encabezado, llame a:
[headerV setNeedsLayout];
[headerV layoutIfNeeded];
  1. Tras el cambio de orientación, vuelva a marcar el encabezado para el diseño y vuelva a cargar la tabla, esto debe suceder un poco después de que se informe el cambio de orientación:
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
fuente
0

En mi caso viewDidLayoutSubviewsfuncionó mejor. viewWillLayoutSubviewshace tableViewque aparezcan líneas blancas de a. También agregué verificar si mi headerViewobjeto ya 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
fuente
0

Es muy posible utilizar un AutoLayout genérico basado UIViewen cualquier estructura de subvista interna de AL como archivo tableHeaderView.

¡Lo único que se necesita es establecer un tableFooterViewantes simple !

Sea self.headerViewalgo basado en restricciones 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;
}

Si self.headerViewcambia la altura bajo la rotación de la interfaz de usuario, se debe 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];
}

Se puede usar la categoría ObjC para este propósito

@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

Y también extensión 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
fuente
0

Esta solución me funciona perfectamente:

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

Extiende el UITableViewController. Pero si solo está usando un UITableView, seguirá funcionando, simplemente extienda UITableView en lugar de UITableViewController. Llame a los métodos sizeHeaderToFit()o sizeFooterToFit()siempre que haya un evento que cambie la tableViewHeaderaltura.

miau2x
fuente
0

Copiado de esta publicación

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
fuente