J'ai une sous-classe UIView
qui contient une UILabel
à plusieurs lignes. Cette vue utilise autolayout.
J'aimerais définir cette vue comme étant la tableHeaderView
d'une UITableView
( pas un en-tête de section). La hauteur de cet en-tête dépend du texte de l'étiquette, qui à son tour dépend de la largeur de l'appareil. Le type de scénario avec autolayout devrait être excellent.
J'ai trouvé et essayé plusieurs _ { plusieurs } _ _ { solutions _ pour que cela fonctionne, mais en vain. Certaines des choses que j'ai essayées:
preferredMaxLayoutWidth
sur chaque étiquette pendant layoutSubviews
intrinsicContentSize
tableHeaderView
manuellement.Quelques échecs rencontrés:
Auto Layout still required after executing -layoutSubviews
La solution (ou les solutions, si nécessaire) devrait fonctionner pour iOS 7 et iOS 8. Notez que tout cela est effectué par programme. J'ai mis en place un petit exemple de projet } au cas où vous souhaiteriez le pirater pour voir le problème. J'ai réinitialisé mes efforts au point de départ suivant:
SCAMessageView *header = [[SCAMessageView alloc] init];
header.titleLabel.text = @"Warning";
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.";
self.tableView.tableHeaderView = header;
Qu'est-ce que je rate?
Ma meilleure réponse jusqu'ici consiste à définir la variable tableHeaderView
une fois et à forcer une passe de présentation. Cela permet de mesurer une taille requise, que j'utilise ensuite pour définir le cadre de l'en-tête. Et, comme il est courant avec tableHeaderView
s, je dois le redéfinir une seconde fois pour appliquer le changement.
- (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;
}
Pour les étiquettes multilignes, cela dépend également de la vue personnalisée (la vue du message dans ce cas) définissant la preferredMaxLayoutWidth
de chaque:
- (void)layoutSubviews
{
[super layoutSubviews];
self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame);
self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame);
}
Malheureusement, cela semble encore nécessaire. Voici une version Swift du processus de mise en page:
tableView.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
var frame = header.frame
frame.size.height = height
header.frame = frame
tableView.tableHeaderView = header
J'ai trouvé utile de déplacer ceci dans une extension sur 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()
let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
var frame = header.frame
frame.size.height = height
header.frame = frame
self.tableHeaderView = header
}
}
Usage:
let header = SCAMessageView()
header.titleLabel.text = "Warning"
header.subtitleLabel.text = "Warning message here."
tableView.setAndLayoutTableHeaderView(header)
Pour ceux qui recherchent toujours une solution, ceci concerne Swift 3 et iOS 9+. En voici une qui utilise uniquement AutoLayout. Il met également à jour correctement la rotation du périphérique.
extension UITableView {
// 1.
func setTableHeaderView(headerView: UIView) {
headerView.translatesAutoresizingMaskIntoConstraints = false
self.tableHeaderView = headerView
// ** Must setup AutoLayout after set tableHeaderView.
headerView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
headerView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
headerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
}
// 2.
func shouldUpdateHeaderViewFrame() -> Bool {
guard let headerView = self.tableHeaderView else { return false }
let oldSize = headerView.bounds.size
// Update the size
headerView.layoutIfNeeded()
let newSize = headerView.bounds.size
return oldSize != newSize
}
}
Utiliser:
override func viewDidLoad() {
...
// 1.
self.tableView.setTableHeaderView(headerView: customView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 2. Reflect the latest size in tableHeaderView
if self.tableView.shouldUpdateHeaderViewFrame() {
// **This is where table view's content (tableHeaderView, section headers, cells)
// frames are updated to account for the new table header size.
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
Gist est que vous devez laisser tableView
gérer le cadre de tableHeaderView
de la même manière que les cellules de la vue tableau. Cela se fait par le biais de tableView
's beginUpdates/endUpdates
.
Le problème est que tableView
ne se soucie pas de la mise en forme automatique lorsqu'elle met à jour les images enfants. Il utilise la tailleactuelletableHeaderView
pour déterminer l'emplacement du premier en-tête de cellule/section.
1) Ajoutez une contrainte de largeur pour que la variable tableHeaderView
utilise cette largeur chaque fois que nous appelons layoutIfNeeded (). Ajoutez également des contraintes centerX et top pour le positionner correctement par rapport à tableView
.
2) Pour que tableView
connaisse la dernière taille de tableHeaderView
, par exemple, lorsque le périphérique est pivoté, dans viewDidLayoutSubviews, nous pouvons appeler layoutIfNeeded () sur tableHeaderView
. Ensuite, si la taille est modifiée, appelez beginUpdates/endUpdates.
Notez que je n'inclue pas beginUpdates/endUpdates dans une fonction, car nous pourrions vouloir différer l'appel ultérieurement.
L'extension UITableView
suivante résout tous les problèmes courants d'autolayouting et de positionnement de la variable tableHeaderView
sans héritage d'utilisation de cadre:
@implementation UITableView (AMHeaderView)
- (void)am_insertHeaderView:(UIView *)headerView
{
self.tableHeaderView = headerView;
NSLayoutConstraint *constraint =
[NSLayoutConstraint constraintWithItem: headerView
attribute: NSLayoutAttributeWidth
relatedBy: NSLayoutRelationEqual
toItem: headerView.superview
attribute: NSLayoutAttributeWidth
multiplier: 1.0
constant: 0.0];
[headerView.superview addConstraint:constraint];
[headerView layoutIfNeeded];
NSArray *constraints = headerView.constraints;
[headerView removeConstraints:constraints];
UIView *layoutView = [UIView new];
layoutView.translatesAutoresizingMaskIntoConstraints = NO;
[headerView insertSubview:layoutView atIndex:0];
[headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];
[headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];
[headerView addConstraints:constraints];
self.tableHeaderView = headerView;
[headerView layoutIfNeeded];
}
@end
Explication des étapes "étranges":
Au début, nous associons la largeur de headerView à la largeur de tableView: cela aide en tant que rotation et empêche le décalage profond vers la gauche des vues secondaires centrées sur X de la headerView.
( the Magic! ) Nous insérons un faux layoutView dans l'en-tête View: À ce moment, nous devons VIVEMENT supprimer toutes les contraintes d'en-têteView, développez layoutView en headerView, puis nous restaurons l'en-tête initial. contraintes. Il arrive que l'ordre des contraintes ait un sens! De la manière dont nous obtenons le calcul automatique headerView correct et également correct
Centralisation X pour toutes les sous-vues headerView.
Ensuite, il suffit de ré-agencer headerView pour obtenir le bon tableauView
Calcul de la hauteur et en-tête Afficher le positionnement au-dessus des sections sans s'entrecroisant.
P.S. Cela fonctionne aussi pour iOS8. Il est impossible de commenter une chaîne de code ici dans un cas ordinaire.
Certaines des réponses ici m'ont aidé à me rapprocher de ce dont j'avais besoin. Mais j'ai rencontré des conflits avec la contrainte "UIView-Encapsulated-Layout-Width" définie par le système lors de la rotation du périphérique entre portrait et paysage. Ma solution ci-dessous est largement basée sur cette Gist de marcoarment (crédit à lui): https://Gist.github.com/marcoarment/1105553afba6b4900c10 . La solution ne repose pas sur la vue en-tête contenant un UILabel. Il y a 3 parties:
func rr_layoutTableHeaderView(width:CGFloat) {
// remove headerView from tableHeaderView:
guard let headerView = self.tableHeaderView else { return }
headerView.removeFromSuperview()
self.tableHeaderView = nil
// create new superview for headerView (so that autolayout can work):
let temporaryContainer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
temporaryContainer.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(temporaryContainer)
temporaryContainer.addSubview(headerView)
// set width constraint on the headerView and calculate the right size (in particular the height):
headerView.translatesAutoresizingMaskIntoConstraints = false
let temporaryWidthConstraint = NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: width)
temporaryWidthConstraint.priority = 999 // necessary to avoid conflict with "UIView-Encapsulated-Layout-Width"
headerView.addConstraint(temporaryWidthConstraint)
headerView.frame.size = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
// remove the temporary constraint:
headerView.removeConstraint(temporaryWidthConstraint)
headerView.translatesAutoresizingMaskIntoConstraints = true
// put the headerView back into the tableHeaderView:
headerView.removeFromSuperview()
temporaryContainer.removeFromSuperview()
self.tableHeaderView = headerView
}
override func viewDidLoad() {
super.viewDidLoad()
// build the header view using autolayout:
let button = UIButton()
let label = UILabel()
button.setTitle("Tap here", for: .normal)
label.text = "The text in this header will span multiple lines if necessary"
label.numberOfLines = 0
let headerView = UIStackView(arrangedSubviews: [button, label])
headerView.axis = .horizontal
// assign the header view:
self.tableView.tableHeaderView = headerView
// continue with other things...
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tableView.rr_layoutTableHeaderView(width: view.frame.width)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.tableView.rr_layoutTableHeaderView(width: size.width)
}
Cela devrait faire l'affaire pour une vue d'en-tête ou une vue de pied de page pour la vue Tableau à l'aide de la mise en forme automatique.
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
var tableFooterViewWithAutolayout: UIView? {
set (view) {
tableFooterView = view
if let view = view {
lowerPriorities(view)
view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
tableFooterView = view
}
}
get {
return tableFooterView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}
Utilisation de l'extension dans Swift 3.0
extension UITableView {
func setTableHeaderView(headerView: UIView?) {
// set the headerView
tableHeaderView = headerView
// check if the passed view is nil
guard let headerView = headerView else { return }
// check if the tableHeaderView superview view is nil just to avoid
// to use the force unwrapping later. In case it fail something really
// wrong happened
guard let tableHeaderViewSuperview = tableHeaderView?.superview else {
assertionFailure("This should not be reached!")
return
}
// force updated layout
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
// set tableHeaderView width
tableHeaderViewSuperview.addConstraint(headerView.widthAnchor.constraint(equalTo: tableHeaderViewSuperview.widthAnchor, multiplier: 1.0))
// set tableHeaderView height
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
tableHeaderViewSuperview.addConstraint(headerView.heightAnchor.constraint(equalToConstant: height))
}
func setTableFooterView(footerView: UIView?) {
// set the footerView
tableFooterView = footerView
// check if the passed view is nil
guard let footerView = footerView else { return }
// check if the tableFooterView superview view is nil just to avoid
// to use the force unwrapping later. In case it fail something really
// wrong happened
guard let tableFooterViewSuperview = tableFooterView?.superview else {
assertionFailure("This should not be reached!")
return
}
// force updated layout
footerView.setNeedsLayout()
footerView.layoutIfNeeded()
// set tableFooterView width
tableFooterViewSuperview.addConstraint(footerView.widthAnchor.constraint(equalTo: tableFooterViewSuperview.widthAnchor, multiplier: 1.0))
// set tableFooterView height
let height = footerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
tableFooterViewSuperview.addConstraint(footerView.heightAnchor.constraint(equalToConstant: height))
}
}