¿Es posible utilizar AutoLayout con tableHeaderView de UITableView?

Desde que descubrí AutoLayout lo uso en todas partes, ahora estoy tratando de usarlo con tableHeaderView .

Hice una subclass de UIView agregó todo (tags, etc …) que quería con sus limitaciones, luego agregué este CustomView a UITableViewtableHeaderView .

Todo funciona bien, excepto que UITableView siempre se muestra arriba de CustomView , por encima quiero decir que CustomView está debajo de UITableView así que no se puede ver!

Parece que no importa lo que haga, la height de UITableViewtableHeaderView siempre es 0 (igual que el ancho, xey).

Mi pregunta: ¿es posible lograr esto sin configurar el marco manualmente ?

EDITAR: La subview CustomView ‘que estoy usando tiene estas restricciones:

 _title = [[UILabel alloc]init]; _title.text = @"Title"; [self addSubview:_title]; [_title keep:[KeepTopInset rules:@[[KeepEqual must:5]]]]; // title has to stay at least 5 away from the supperview Top [_title keep:[KeepRightInset rules:@[[KeepMin must:5]]]]; [_title keep:[KeepLeftInset rules:@[[KeepMin must:5]]]]; [_title keep:[KeepBottomInset rules:@[[KeepMin must:5]]]]; 

Estoy usando una práctica biblioteca ‘KeepLayout’ porque escribir restricciones manualmente toma una y otra vez para una sola restricción, pero los métodos son autoexplicativos.

Y UITableView tiene estas restricciones:

 _tableView = [[UITableView alloc]init]; _tableView.translatesAutoresizingMaskIntoConstraints = NO; _tableView.delegate = self; _tableView.dataSource = self; _tableView.backgroundColor = [UIColor clearColor]; [self.view addSubview:_tableView]; [_tableView keep:[KeepTopInset rules:@[[KeepEqual must:0]]]];// These 4 constraints make the UITableView stays 0 away from the superview top left right and bottom. [_tableView keep:[KeepLeftInset rules:@[[KeepEqual must:0]]]]; [_tableView keep:[KeepRightInset rules:@[[KeepEqual must:0]]]]; [_tableView keep:[KeepBottomInset rules:@[[KeepEqual must:0]]]]; _detailsView = [[CustomView alloc]init]; _tableView.tableHeaderView = _detailsView; 

No sé si tengo que establecer algunas restricciones directamente en el CustomView , creo que la altura del CustomView está determinada por las restricciones en el “título” de UILabel .

EDIT 2: después de otra investigación, parece que el alto y el ancho de CustomView se calculan correctamente, pero la parte superior de CustomView sigue estando en el mismo nivel que la parte superior de UITableView y se mueven juntas cuando me desplazo.

Pregunté y respondí una pregunta similar aquí . En resumen, agrego el encabezado una vez y lo uso para encontrar la altura requerida. Esa altura se puede aplicar al encabezado y el encabezado se establece por segunda vez para reflejar el cambio.

 - (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's frame and set it again CGRect headerFrame = self.header.frame; headerFrame.size.height = height; self.header.frame = headerFrame; self.tableView.tableHeaderView = self.header; } 

Si tiene tags de múltiples líneas, esto también se basa en la configuración de vista personalizada de la preferenciaMaxLayoutWidth de cada etiqueta:

 - (void)layoutSubviews { [super layoutSubviews]; self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame); self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame); } 

o quizás más en general:

 override func layoutSubviews() { super.layoutSubviews() for view in subviews { guard let label = view as? UILabel where label.numberOfLines == 0 else { continue } label.preferredMaxLayoutWidth = CGRectGetWidth(label.frame) } } 

Actualización enero de 2015

Lamentablemente, esto parece ser necesario. Aquí hay una versión rápida del proceso de diseño:

 tableView.tableHeaderView = header header.setNeedsLayout() header.layoutIfNeeded() header.frame.size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) tableView.tableHeaderView = header 

Me pareció útil mover esto a una extensión en UITableView:

 extension UITableView { //set the tableHeaderView so that the required height can be determined, update the header's frame and set it again func setAndLayoutTableHeaderView(header: UIView) { self.tableHeaderView = header header.setNeedsLayout() header.layoutIfNeeded() header.frame.size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) self.tableHeaderView = header } } 

Uso:

 let header = SCAMessageView() header.titleLabel.text = "Warning" header.subtitleLabel.text = "Warning message here." tableView.setAndLayoutTableHeaderView(header) 

No he podido agregar una vista de encabezado usando restricciones (en el código). Si doy a mi vista una restricción de ancho y / o altura, recibo un locking con el mensaje que dice:

  "terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super." 

Cuando agrego una vista en el guión gráfico a mi vista de tabla, no muestra restricciones, y funciona bien como vista de encabezado, por lo que creo que la ubicación de la vista de encabezado no se hace usando restricciones. No parece comportarse como una visión normal en ese sentido.

El ancho es automáticamente el ancho de la vista de tabla, lo único que necesita establecer es la altura: los valores de origen se ignoran, por lo que no importa lo que ponga para eso. Por ejemplo, esto funcionó bien (como lo hace 0,0,0,80 para el rect):

 UIView *headerview = [[UIView alloc] initWithFrame:CGRectMake(1000,1000, 0, 80)]; headerview.backgroundColor = [UIColor yellowColor]; self.tableView.tableHeaderView = headerview; 

Vi muchos métodos aquí haciendo tantas cosas innecesarias, pero no necesita mucho para usar el diseño automático en la vista de encabezado. Solo tiene que crear su archivo xib, poner sus restricciones y crear una instancia de esta manera:

 func loadHeaderView () { guard let headerView = Bundle.main.loadNibNamed("CourseSearchHeader", owner: self, options: nil)?[0] as? UIView else { return } headerView.autoresizingMask = .flexibleWidth headerView.translatesAutoresizingMaskIntoConstraints = true tableView.tableHeaderView = headerView } 

Otra solución es enviar la creación de vista de encabezado a la siguiente llamada de subproceso principal:

 - (void)viewDidLoad { [super viewDidLoad]; // .... dispatch_async(dispatch_get_main_queue(), ^{ _profileView = [[MyView alloc] initWithNib:@"MyView.xib"]; self.tableView.tableHeaderView = self.profileView; }); } 

Nota: Soluciona el error cuando la vista cargada tiene una altura fija. No lo he intentado cuando la altura del encabezado solo depende de su contenido.

EDITAR:

Puede encontrar una solución más limpia a este problema implementando esta función y llamándola en viewDidLayoutSubviews

 - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; [self sizeHeaderToFit]; } 

Extendió esta solución http://collindonnell.com/2015/09/29/dynamically-sized-table-view-header-or-footer-using-auto-layout/ para la vista de pie de tabla:

 @interface AutolayoutTableView : UITableView @end @implementation AutolayoutTableView - (void)layoutSubviews { [super layoutSubviews]; // Dynamic sizing for the header view if (self.tableHeaderView) { CGFloat height = [self.tableHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; CGRect headerFrame = self.tableHeaderView.frame; // If we don't have this check, viewDidLayoutSubviews() will get // repeatedly, causing the app to hang. if (height != headerFrame.size.height) { headerFrame.size.height = height; self.tableHeaderView.frame = headerFrame; self.tableHeaderView = self.tableHeaderView; } [self.tableHeaderView layoutIfNeeded]; } // Dynamic sizing for the header view if (self.tableFooterView) { CGFloat height = [self.tableFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; CGRect footerFrame = self.tableFooterView.frame; // If we don't have this check, viewDidLayoutSubviews() will get // repeatedly, causing the app to hang. if (height != footerFrame.size.height) { footerFrame.size.height = height; self.tableFooterView.frame = footerFrame; self.tableFooterView = self.tableFooterView; } self.tableFooterView.transform = CGAffineTransformMakeTranslation(0, self.contentSize.height - footerFrame.size.height); [self.tableFooterView layoutIfNeeded]; } } @end 

Cosas extrañas suceden systemLayoutSizeFittingSize funciona muy bien para iOS9, pero no para iOS 8 en mi caso. Así que este problema se resuelve bastante fácil. Simplemente obtenga un enlace a la vista inferior en el encabezado y en viewDidLayoutSubviews después de los límites de vista del encabezado de actualización de super llamada insertando height como CGRectGetMaxY (yourview.frame) + relleno

UPD: la solución más fácil de todas : por lo tanto, en la vista de encabezado, coloque la subvista y gírela a la izquierda , derecha , arriba . En esa subvista, coloque sus subvistas con restricciones de altura automática. Después de eso, dale todo el trabajo a la salida automática (no se requiere cálculo)

 - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat height = CGRectGetMaxY(self.tableView.tableHeaderView.subviews.firstObject.frame); self.tableView.tableHeaderView.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds), height); self.tableView.tableHeaderView = self.tableView.tableHeaderView; } 

Como resultado, la subvista se está expandiendo / contrayendo como debería, al final llama a viewDidLayoutSubviews. En el momento en que conocemos el tamaño real de la vista, configure la altura del encabezado y actualícela reasignando. ¡Funciona de maravilla!

También funciona para la vista de pie de página.

Puede obtener el diseño automático para proporcionarle un tamaño utilizando el método systemLayoutSizeFittingSize .

Puede usar esto para crear el marco para su aplicación. Esta técnica funciona siempre que necesite conocer el tamaño de una vista que utiliza el autolayout internamente.

El código en swift se parece a

 //Create the view let tableHeaderView = CustomTableHeaderView() //Set the content tableHeaderView.textLabel.text = @"Hello world" //Ask auto layout for the smallest size that fits my constraints let size = tableHeaderView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) //Create a frame tableHeaderView.frame = CGRect(origin: CGPoint.zeroPoint, size: size) //Set the view as the header self.tableView.tableHeaderView = self.tableHeaderView 

O en Objective-C

 //Create the view CustomTableHeaderView *header = [[CustomTableHeaderView alloc] initWithFrame:CGRectZero]; //Set the content header.textLabel.text = @"Hello world"; //Ask auto layout for the smallest size that fits my constraints CGSize size = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; //Create a frame header.frame = CGRectMake(0,0,size.width,size.height); //Set the view as the header self.tableView.tableHeaderView = header 

También se debe tener en cuenta que en esta instancia particular, overriding requiereConstraintBasedLayout en su subclase, da como resultado que se realice una pasada de diseño, sin embargo, los resultados de este pase de diseño se ignoran y el marco del sistema se establece con el ancho de tableView y 0 height.

Código:

  extension UITableView { func sizeHeaderToFit(preferredWidth: CGFloat) { guard let headerView = self.tableHeaderView else { return } headerView.translatesAutoresizingMaskIntoConstraints = false let layout = NSLayoutConstraint( item: headerView, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: preferredWidth) headerView.addConstraint(layout) let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height headerView.frame = CGRectMake(0, 0, preferredWidth, height) headerView.removeConstraint(layout) headerView.translatesAutoresizingMaskIntoConstraints = true self.tableHeaderView = headerView } } 

Lo siguiente funcionó para mí.

  1. Use un viejo UIView como la vista de encabezado.
  2. Añadir subvistas a esa UIView
  3. Use el autodiseño en las subvistas

El principal beneficio que veo es la limitación de los cálculos de marcos. Apple realmente debería actualizar la API de UITableView para hacer esto más fácil.

Ejemplo usando SnapKit:

 let layoutView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 60)) layoutView.backgroundColor = tableView.backgroundColor tableView.tableHeaderView = layoutView let label = UILabel() layoutView.addSubview(label) label.text = "I'm the view you really care about" label.snp_makeConstraints { make in make.edges.equalTo(EdgeInsets(top: 10, left: 15, bottom: -5, right: -15)) } 

Mi vista de encabezado de tabla es una subclase de UIView: creé una UView única de contentView dentro del inicializador, con sus límites iguales al marco de la vista de encabezado de tabla y agregué todos mis objetos como una subvista de eso.

A continuación, agregue las restricciones para sus objetos dentro del método layoutSubviews la vista de encabezado de la layoutSubviews lugar de dentro del inicializador. Eso resolvió el accidente.

 - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:CGRectMake(0, 0, 0, 44.0)]; if (self) { UIView *contentView = [[UIView alloc] initWithFrame:self.bounds]; contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth; // add other objects as subviews of content view } return self; } - (void)layoutSubviews { [super layoutSubviews]; // remake constraints here } 

puede agregar una restricción de ubicación superior + horizontal entre el encabezado y la vista de tabla, para colocarla, correctamente (si el encabezado en sí contiene todas las restricciones de diseño interno necesarias para tener un marco correcto)

en el método tableViewController viewDidLoad

  headerView.translatesAutoresizingMaskIntoConstraints = false tableView.tableHeaderView = headerView headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true headerView.topAnchor.constraint(equalTo: tableView.topAnchor).isActive = true headerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor).isActive = true 

Sé que esta es una publicación anterior, pero después de pasar por todas las publicaciones de SO sobre esto y pasar toda una tarde jugando con esto, finalmente se me ocurrió una solución limpia pero muy simple.

Antes que nada, la jerarquía de mi vista se ve así:

  1. Vista de tabla
    1. Ver tableHeaderView
      1. Ver con una salida llamada headerView

Ahora dentro de la Vista (No.3), configuré todas las restricciones como lo haría normalmente, incluyendo el espacio inferior al contenedor. Esto hará que el contenedor (es decir, 3.View ie headerView) se dimensione en función de sus subvistas y sus restricciones.

Después de eso, establezco las restricciones entre 3. View y 2. View a estos:

  1. Espacio superior al contenedor: 0
  2. Líder de espacio al contenedor: 0
  3. Espacio al final del contenedor: 0

Tenga en cuenta que omito intencionalmente el espacio inferior intencionalmente.

Una vez que todo esto se hace en el guión gráfico, todo lo que queda por hacer es pegar esas tres líneas de códigos:

 if (self.headerView.frame.size.height != self.tableView.tableHeaderView.frame.size.height) { UIView *header = self.tableView.tableHeaderView; CGRect frame = self.tableView.tableHeaderView.frame; frame.size.height = self.headerView.frame.size.height + frame.origin.y; header.frame = frame; self.tableView.tableHeaderView = header; } 

Sugerencias: si usa el método setAndLayoutTableHeaderView, debe actualizar el marco de las subvistas, por lo que, en esta situación, el preferredMaxLayoutWidth de UILabel debe llamar antes de invocar systemLayoutSizeFittingSize, no invoque el layoutSubview.

mostrar código

Comparte mi enfoque.

UITableView+XXXAdditions.m

 - (void)xxx_setTableHeaderView:(UIView *)tableHeaderView layoutBlock:(void(^)(__kindof UIView *tableHeaderView, CGFloat *containerViewHeight))layoutBlock { CGFloat containerViewHeight = 0; UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectZero]; [backgroundView addSubview:tableHeaderView]; layoutBlock(tableHeaderView, &containerViewHeight); backgroundView.frame = CGRectMake(0, 0, 0, containerViewHeight); self.tableHeaderView = backgroundView; } 

Uso.

 [self.tableView xxx_setTableHeaderView:myView layoutBlock:^(__kindof UIView * _Nonnull tableHeaderView, CGFloat *containerViewHeight) { *containerViewHeight = 170; [tableHeaderView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(@20); make.centerX.equalTo(@0); make.size.mas_equalTo(CGSizeMake(130, 130)); }]; }]; 

En mi caso, el método con systemLayoutSizeFittingSize por alguna razón no funcionó. Lo que funcionó para mí es una modificación de la solución publicada por HotJard (su solución original tampoco funcionó en mi caso en iOS 8). Lo que tenía que hacer es colocar en la vista de encabezado una subvista y fijarla a la izquierda, a la derecha, arriba (no fijar al fondo). Pon todo usando autolayout en esa subvista y en el código haz esto:

 - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; [self resizeHeaderToFitSubview]; } - (void)resizeHeaderToFitSubview { UIView *header = self.tableView.tableHeaderView; [header setNeedsLayout]; [header layoutIfNeeded]; CGFloat height = CGRectGetHeight(header.subviews.firstObject.bounds); header.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds), height); self.tableView.tableHeaderView = nil; self.tableView.tableHeaderView = header; } 

Una publicación anterior. Pero una buena publicación. Aquí están mis 2 centavos.

En primer lugar, asegúrese de que su vista de encabezado tenga sus restricciones organizadas para que pueda admitir su propio tamaño de contenido intrínseco. Entonces haz lo siguiente.

 //ViewDidLoad headerView.translatesAutoresizingMaskIntoConstraints = false headerView.configure(title: "Some Text A") //Somewhere else headerView.update(title: "Some Text B) private var widthConstrained = false override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if widthConstrained == false { widthConstrained = true tableView.addConstraint(NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: tableView, attribute: .width, multiplier: 1, constant: 0)) headerView.layoutIfNeeded() tableView.layoutIfNeeded() } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { (context) in self.headerView.layoutIfNeeded() self.tableView.layoutIfNeeded() }, completion: nil) } 

Pude lograrlo con el siguiente enfoque (esto funciona para el pie de página de la misma manera).

Primero, necesitarás una pequeña extensión UITableView :

Swift 3

 extension UITableView { fileprivate func adjustHeaderHeight() { if let header = self.tableHeaderView { adjustFrame(header) } } private func adjustFrame(_ view: UIView) { view.frame.size.height = calculatedViewHeight(view) } fileprivate func calculatedHeightForHeader() -> CGFloat { if let header = self.tableHeaderView { return calculatedViewHeight(header) } return 0.0 } private func calculatedViewHeight(_ view: UIView) -> CGFloat { view.setNeedsLayout() let height = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height return height } } 

En su vista implementación de la clase de controlador:

 // this is a UIView subclass with autolayout private var headerView = MyHeaderView() override func loadView() { super.loadView() // ... self.tableView.tableHeaderView = headerView self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension // ... } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() // this is to prevent recursive layout calls let requiredHeaderHeight = self.tableView.calculatedHeightForHeader() if self.headerView.frame.height != requiredHeaderHeight { self.tableView.adjustHeaderHeight() } } 

Notas sobre un encabezado UIView de subvista de UIView :

  1. Tienes que estar 100% seguro de que tu vista de encabezado tiene la configuración correcta de autodiseño. Recomiendo comenzar con una vista de encabezado simple con solo una restricción de altura y probar la configuración anterior.

  2. La anulación requiresConstraintBasedLayout y devuelve true :

.

 class MyHeaderView: UIView { // ... override static var requiresConstraintBasedLayout : Bool { return true } // ... } 

Mi AutoLayout está funcionando muy bien:

 CGSize headerSize = [headerView systemLayoutSizeFittingSize:CGSizeMake(CGRectGetWidth([UIScreen mainScreen].bounds), 0) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel]; headerView.frame = CGRectMake(0, 0, headerSize.width, headerSize.height); self.tableView.tableHeaderView = headerView; 

Así es como puedes hacerlo en tu UIViewController

 override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if headerView.frame.size.height == 0 { headerView.label.preferredMaxLayoutWidth = view.bounds.size.width - 20 let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height headerView.frame.size = CGSize(width: tableView.bounds.size.width, height: height) } } 

Cualquier UIView basado en UIView puede ser un buen tableHeaderView .

Uno necesita establecer tableFooterView antes y luego imponer restricciones adicionales al final en tableFooterView y tableHeaderView .

 - (void)viewDidLoad { ........................ // let self.headerView is some constraint-based UIView self.tableView.tableFooterView = [UIView new]; [self.headerView layoutIfNeeded]; 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; [self.tableFooterView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES; 

}

Uno puede encontrar todos los detalles y fragmentos de código aquí

La respuesta aceptada es solo útil para tablas con una sola sección. Para multi-sección UITableView solo asegúrese de que su encabezado herede de UITableViewHeaderFooterView y estará bien.

Como alternativa, solo incruste su encabezado actual en contentView de UITableViewHeaderFooterView . Exactamente como funciona UITableViewCell .

Para usuarios de Xamarin:

 public override void ViewDidLayoutSubviews() { base.ViewDidLayoutSubviews(); TableviewHeader.SetNeedsLayout(); TableviewHeader.LayoutIfNeeded(); var height = TableviewHeader.SystemLayoutSizeFittingSize(UIView.UILayoutFittingCompressedSize).Height; var frame = TableviewHeader.Frame; frame.Height = height; TableviewHeader.Frame = frame; } 

Suponiendo que haya nombrado la vista de encabezado de su tabla vista como TableviewHeader