web-dev-qa-db-fra.com

Utilisation de l'autolayout dans une tableHeaderView

J'ai une sous-classe UIView qui contient une UILabel à plusieurs lignes. Cette vue utilise autolayout.

enter image description here

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:

  • définir une preferredMaxLayoutWidth sur chaque étiquette pendant layoutSubviews
  • définir une intrinsicContentSize
  • en essayant de déterminer la taille requise pour la vue et en définissant le cadre de la tableHeaderView manuellement.
  • ajout d'une contrainte de largeur à la vue lorsque l'en-tête est défini
  • un tas d'autres choses

Quelques échecs rencontrés:

  • l'étiquette s'étend au-delà de la largeur de la vue, sans envelopper
  • la hauteur du cadre est 0
  • l'application se bloque avec l'exception 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?

14
Ben Packard

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 tableHeaderViews, 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);
}

Mise à jour janvier 2015

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)
20
Ben Packard

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.

Découvrez un exemple de projet

5
aunnnn

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":

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

  2. ( 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.

  3. 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.

2
malex

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:

  1. Une fonction définie dans une extension de UITableView.
  2. Appelez la fonction à partir de viewWillAppear () du contrôleur de vue.
  3. Appelez la fonction à partir de viewWillTransition () du contrôleur de vue afin de gérer la rotation du périphérique.

UITableView extension

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
}

Utilisation dans UITableViewController

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)
}
2
rene

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)
      }
    }
  }
}
0
Everton Cunha

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))
    }
}
0
Carmelo Gallo