2013-03-08 3 views
24

Je suis en train de construire un titleview avec des contraintes qui ressemble à ceci:Construire une titleview programmation avec des contraintes (ou la construction en général en vue des contraintes)

titleView

Je sais comment je ferais cela avec cadres. Je voudrais calculer la largeur du texte, la largeur de l'image, créer une vue avec cette largeur/hauteur pour contenir les deux, puis ajouter les deux sous-vues aux emplacements appropriés avec des cadres. J'essaie de comprendre comment on pourrait faire cela avec des contraintes. Ma pensée était que la taille intrinsèque du contenu m'aiderait ici, mais j'essaie désespérément de faire en sorte que cela fonctionne.

UILabel *categoryNameLabel = [[UILabel alloc] init]; 
categoryNameLabel.text = categoryName; // a variable from elsewhere that has a category like "Popular" 
categoryNameLabel.translatesAutoresizingMaskIntoConstraints = NO; 
[categoryNameLabel sizeToFit]; // hoping to set it to the instrinsic size of the text? 

UIView *titleView = [[UIView alloc] init]; // no frame here right? 
[titleView addSubview:categoryNameLabel]; 
NSArray *constraints; 
if (categoryImage) { 
    UIImageView *categoryImageView = [[UIImageView alloc] initWithImage:categoryImage]; 
    [titleView addSubview:categoryImageView]; 
    categoryImageView.translatesAutoresizingMaskIntoConstraints = NO; 
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|[categoryImageView]-[categoryNameLabel]|" options:NSLayoutFormatAlignAllTop metrics:nil views:NSDictionaryOfVariableBindings(categoryImageView, categoryNameLabel)]; 
} else { 
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|[categoryNameLabel]|" options:NSLayoutFormatAlignAllTop metrics:nil views:NSDictionaryOfVariableBindings(categoryNameLabel)]; 
} 
[titleView addConstraints:constraints]; 


// here I set the titleView to the navigationItem.titleView 

Je ne devrais pas avoir à coder en dur la taille de titleView. Elle devrait pouvoir être déterminée en fonction de la taille de son contenu, mais ...

  1. La propriété titleView détermine si sa taille est 0 sauf si je code en dur une trame.
  2. Si je mets translatesAutoresizingMaskIntoConstraints = NO les plantages d'applications avec cette erreur: 'Auto Layout still required after executing -layoutSubviews. UINavigationBar's implementation of -layoutSubviews needs to call super.'

Mise à jour

Je l'ai à travailler avec ce code, mais je suis toujours avoir à définir le cadre de la titleview:

UILabel *categoryNameLabel = [[UILabel alloc] init]; 
categoryNameLabel.translatesAutoresizingMaskIntoConstraints = NO; 
categoryNameLabel.text = categoryName; 
categoryNameLabel.opaque = NO; 
categoryNameLabel.backgroundColor = [UIColor clearColor]; 

UIView *titleView = [[UIView alloc] init]; 
[titleView addSubview:categoryNameLabel]; 
NSArray *constraints; 
if (categoryImage) { 
    UIImageView *categoryImageView = [[UIImageView alloc] initWithImage:categoryImage]; 
    [titleView addSubview:categoryImageView]; 
    categoryImageView.translatesAutoresizingMaskIntoConstraints = NO; 
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|[categoryImageView]-7-[categoryNameLabel]|" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(categoryImageView, categoryNameLabel)]; 
    [titleView addConstraints:constraints]; 
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[categoryImageView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(categoryImageView)]; 
    [titleView addConstraints:constraints]; 

    titleView.frame = CGRectMake(0, 0, categoryImageView.frame.size.width + 7 + categoryNameLabel.intrinsicContentSize.width, categoryImageView.frame.size.height); 
} else { 
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|[categoryNameLabel]|" options:NSLayoutFormatAlignAllTop metrics:nil views:NSDictionaryOfVariableBindings(categoryNameLabel)]; 
    [titleView addConstraints:constraints]; 
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[categoryNameLabel]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(categoryNameLabel)]; 
    [titleView addConstraints:constraints]; 
    titleView.frame = CGRectMake(0, 0, categoryNameLabel.intrinsicContentSize.width, categoryNameLabel.intrinsicContentSize.height); 
} 
return titleView; 
+0

Votre code original n'a probablement pas fonctionné car vous n'aviez aucune contrainte dans l'axe vertical; votre seconde devrait être bien sans avoir à mettre des cadres. Que se passe-t-il si vous créez cette vue et que vous l'ajoutez ailleurs (plutôt que dans une barre de navigation, ce qui peut entraîner une complexité supplémentaire). – jrturton

+1

J'essaye de réaliser ceci sans définir le cadre mais je n'ai pas pu trouver qui est le parent de la vue de titre dans lequel nous sommes censés ajouter les contraintes ... –

+1

Le parent de la vue est le 'UINavigationBar' lui-même - mais vous êtes pas autorisé à ajouter des contraintes pour une raison quelconque - échouer avec 'Impossible de modifier les contraintes pour UINavigationBar géré par un contrôleur'. – Petar

Répondre

7

vous devez définir le cadre de titleView parce que vous ne spécifiez pas de contraintes pour son position dans son superview. Le système Auto Layout peut seulement déterminer le size de titleView pour vous parmi les contraintes que vous avez spécifiées et la intrinsic content size de ses sous-vues.

+0

Je ne pense pas que ce soit le cas cependant, parce que je n'ai pas changé 'translateAutoresizingMaskIntoConstraints', donc il aurait dû utiliser des masques autoresizing par défaut. Si je l'ai fait à NON, alors il aurait dû être capable de déterminer la taille via l'étiquette de texte et l'image à l'intérieur. –

+0

Le redimensionnement automatique a besoin de vous pour définir la taille initiale; Auto Layout a besoin de vous pour spécifier la contrainte. Si vous ne faites ni l'un ni l'autre, aucun d'eux ne peut vous aider. Pensez-y, ou essayez-le simplement - définissez la taille ou la contrainte de métrique pour 'titleView' et vous verrez. – an0

+0

True sur le redimensionnement automatique, mais même lorsque j'ai désactivé 'translatesAutoresizingMaskIntoConstraints', mon titleView devrait être capable de déterminer la taille de ses enfants (étiquette ou étiquette et image) avec les contraintes que j'ai définies, et devrait avoir une taille. Je suppose que c'était le cas, mais comme il n'y avait pas de contraintes pour le placer dans le parent, il ne planterait aucun moyen de déterminer son emplacement. –

18

La réponse d'an0 est correcte. Cependant, cela ne vous aide pas à obtenir l'effet désiré.

Voici ma recette pour construire des vues titre qui ont automatiquement la bonne taille:

  • Créer une sous-classe UIView, par exemple CustomTitleView qui seront plus tard utilisés comme l » titleView du navigationItem. Utilisez la disposition automatique à l'intérieur de CustomTitleView. Si vous souhaitez que votre CustomTitleView soit toujours centré, vous devez ajouter une contrainte CenterX explicite (voir le code et le lien ci-dessous).
  • Appelez updateCustomTitleView (voir ci-dessous) chaque fois que votre contenu titleView est mis à jour. Nous devons redéfinir titleView et le redéfinir sur notre vue pour éviter que l'affichage du titre soit centré. Cela se produirait lorsque la vue du titre changerait de large à étroite.
  • NE PAS désactiver translatesAutoresizingMaskIntoConstraints

Contenu essentiel: https://gist.github.com/bhr/78758bd0bd4549f1cd1c

Mise à jour de votre CustomTitleView ViewController:

- (void)updateCustomTitleView 
{ 
    //we need to set the title view to nil and get always the right frame 
    self.navigationItem.titleView = nil; 

    //update properties of your custom title view, e.g. titleLabel 
    self.navTitleView.titleLabel.text = <#my_property#>; 

    CGSize size = [self.navTitleView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; 
    self.navTitleView.frame = CGRectMake(0.f, 0.f, size.width, size.height); 

    self.navigationItem.titleView = self.customTitleView; 
} 

Exemple CustomTitleView.h avec une étiquette et deux boutons

#import <UIKit/UIKit.h> 

@interface BHRCustomTitleView : UIView 

@property (nonatomic, strong, readonly) UILabel *titleLabel; 
@property (nonatomic, strong, readonly) UIButton *previousButton; 
@property (nonatomic, strong, readonly) UIButton *nextButton; 

@end 

Exemple CustomTitleView.m:

#import "BHRCustomTitleView.h" 

@interface BHRCustomTitleView() 

@property (nonatomic, strong) UILabel *titleLabel; 
@property (nonatomic, strong) UIButton *previousButton; 
@property (nonatomic, strong) UIButton *nextButton; 

@property (nonatomic, copy) NSArray *constraints; 

@end 

@implementation BHRCustomTitleView 

- (void)updateConstraints 
{ 
    if (self.constraints) { 
     [self removeConstraints:self.constraints]; 
    } 

    NSDictionary *viewsDict = @{ @"title": self.titleLabel, 
           @"previous": self.previousButton, 
           @"next": self.nextButton }; 
    NSMutableArray *constraints = [NSMutableArray array]; 

    [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=0)-[previous]-2-[title]-2-[next]-(>=0)-|" 
                      options:NSLayoutFormatAlignAllBaseline 
                      metrics:nil 
                       views:viewsDict]]; 

    [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[previous]|" 
                      options:0 
                      metrics:nil 
                       views:viewsDict]]; 

    [constraints addObject:[NSLayoutConstraint constraintWithItem:self 
                 attribute:NSLayoutAttributeCenterX 
                 relatedBy:NSLayoutRelationEqual 
                  toItem:self.titleLabel 
                 attribute:NSLayoutAttributeCenterX 
                 multiplier:1.f 
                 constant:0.f]]; 
    self.constraints = constraints; 
    [self addConstraints:self.constraints]; 

    [super updateConstraints]; 
} 

- (UILabel *)titleLabel 
{ 
    if (!_titleLabel) 
    { 
     _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 
     _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; 
     _titleLabel.font = [UIFont boldSystemFontOfSize:_titleLabel.font.pointSize]; 

     [self addSubview:_titleLabel]; 
    } 

    return _titleLabel; 
} 


- (UIButton *)previousButton 
{ 
    if (!_previousButton) 
    { 
     _previousButton = [UIButton buttonWithType:UIButtonTypeSystem]; 
     _previousButton.translatesAutoresizingMaskIntoConstraints = NO; 
     [self addSubview:_previousButton]; 

     _previousButton.titleLabel.font = [UIFont systemFontOfSize:23.f]; 
     [_previousButton setTitle:@"❮" 
         forState:UIControlStateNormal]; 
    } 

    return _previousButton; 
} 

- (UIButton *)nextButton 
{ 
    if (!_nextButton) 
    { 
     _nextButton = [UIButton buttonWithType:UIButtonTypeSystem]; 
     _nextButton.translatesAutoresizingMaskIntoConstraints = NO; 
     [self addSubview:_nextButton]; 
     _nextButton.titleLabel.font = [UIFont systemFontOfSize:23.f]; 
     [_nextButton setTitle:@"❯" 
        forState:UIControlStateNormal]; 
    } 

    return _nextButton; 
} 

+ (BOOL)requiresConstraintBasedLayout 
{ 
    return YES; 
} 

@end 
+0

Merci de mentionner que vous ne devez pas désactiver 'translatesAutoresizingMaskIntoConstraints'. J'avais défini un élément qui était précédemment affiché ailleurs comme le 'titleView', mais il serait toujours mal positionné après la navigation. Supprimer cette ligne l'a corrigé. – Livven

+0

La vue de mon titre est affichée au centre, ses sous-vues apparaissent en (0,0) dans les coordonnées de la fenêtre (en haut à gauche). Je fais essentiellement la même chose que vous, sauf pour utiliser l'API des ancres pour les contraintes. –

5

Pour combiner les contraintes d'auto-mise à l'intérieur titleView et hardcoded logique de mise en page à l'intérieur UINavigationBar vous devez implémenter la méthode sizeThatFits: dans votre propre classe de mesure titleView (sous-classe de UIView) comme ceci:

- (CGSize)sizeThatFits:(CGSize)size 
{ 
    return CGSizeMake(
     CGRectGetWidth(self.imageView.bounds) + CGRectGetWidth(self.labelView.bounds) + 5.f /* space between icon and text */, 
     MAX(CGRectGetHeight(self.imageView.bounds), CGRectGetHeight(self.labelView.bounds)) 
    ); 
} 
8

Merci @Valentin Shergin et @tubtub! Selon leurs réponses que je fait une mise en œuvre du titre de la barre de navigation avec l'image déroulante flèche Swift 1.2:

  1. Créer une sous-classe UIView pour la coutume titleView
  2. Dans votre sous-classe: a) Utiliser la mise en page automatique pour les sous-vues, mais pas pour lui-même. Définir translatesAutoresizingMaskIntoConstraints à false pour les sous-vues et true pour titleView lui-même. b) Mettre en œuvre sizeThatFits(size: CGSize)
  3. Si votre titre peut changer appel titleLabel.sizeToFit() et self.setNeedsUpdateConstraints() dans la sous-classe de titleView après le texte change
  4. Dans votre appel ViewController personnalisé updateTitleView() et assurez-vous d'appeler titleView.sizeToFit() et navigationBar.setNeedsLayout()

Voici la mise en œuvre minimale de DropdownTitleView:

import UIKit 

class DropdownTitleView: UIView { 

    private var titleLabel: UILabel 
    private var arrowImageView: UIImageView 

    // MARK: - Life cycle 

    override init (frame: CGRect) { 

     self.titleLabel = UILabel(frame: CGRectZero) 
     self.titleLabel.setTranslatesAutoresizingMaskIntoConstraints(false) 

     self.arrowImageView = UIImageView(image: UIImage(named: "dropdown-arrow")!) 
     self.arrowImageView.setTranslatesAutoresizingMaskIntoConstraints(false) 

     super.init(frame: frame) 

     self.setTranslatesAutoresizingMaskIntoConstraints(true) 
     self.addSubviews() 
    } 

    convenience init() { 
     self.init(frame: CGRectZero) 
    } 

    required init(coder aDecoder: NSCoder) { 
     fatalError("DropdownTitleView does not support NSCoding") 
    } 

    private func addSubviews() { 
     addSubview(titleLabel) 
     addSubview(arrowImageView) 
    } 

    // MARK: - Methods 

    func setTitle(title: String) { 
     titleLabel.text = title 
     titleLabel.sizeToFit() 
     setNeedsUpdateConstraints() 
    } 

    // MARK: - Layout 

    override func updateConstraints() { 
     removeConstraints(self.constraints()) 

     let viewsDictionary = ["titleLabel": titleLabel, "arrowImageView": arrowImageView] 
     var constraints: [AnyObject] = [] 

     constraints.extend(NSLayoutConstraint.constraintsWithVisualFormat("H:|[titleLabel]-8-[arrowImageView]|", options: .AlignAllBaseline, metrics: nil, views: viewsDictionary)) 
     constraints.extend(NSLayoutConstraint.constraintsWithVisualFormat("V:|[titleLabel]|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDictionary)) 

     self.addConstraints(constraints) 

     super.updateConstraints() 
    } 

    override func sizeThatFits(size: CGSize) -> CGSize { 
     // +8.0 - distance between image and text 
     let width = CGRectGetWidth(arrowImageView.bounds) + CGRectGetWidth(titleLabel.bounds) + 8.0 
     let height = max(CGRectGetHeight(arrowImageView.bounds), CGRectGetHeight(titleLabel.bounds)) 
     return CGSizeMake(width, height) 
    } 
} 

et ViewController:

override func viewDidLoad() { 
    super.viewDidLoad() 

    // Set custom title view to show arrow image along with title 
    self.navigationItem.titleView = dropdownTitleView 

    // your code ... 
} 

private func updateTitleView(title: String) { 
    // update text 
    dropdownTitleView.setTitle(title) 

    // layout title view 
    dropdownTitleView.sizeToFit() 
    self.navigationController?.navigationBar.setNeedsLayout() 
} 
+0

Impossible de le faire fonctionner. Les rects liés calculés dans 'sizeThatFits()' renvoient zéro pour la largeur et la hauteur. –

47

J'avais vraiment besoin de contraintes, donc j'ai joué avec ça aujourd'hui. Ce que j'ai trouvé qui fonctionne est la suivante:

let v = UIView() 
    v.translatesAutoresizingMaskIntoConstraints = false 
    // add your views and set up all the constraints 

    // This is the magic sauce! 
    v.layoutIfNeeded() 
    v.sizeToFit() 

    // Now the frame is set (you can print it out) 
    v.translatesAutoresizingMaskIntoConstraints = true // make nav bar happy 
    navigationItem.titleView = v 

Fonctionne comme un charme!

+2

Je souhaite que j'ai trouvé cette réponse plus tôt parce que je viens de faire fonctionner cela, bien qu'en utilisant 'sizeThatFits' pour toutes les sous-vues, puis en calculant le cadre de la vue après cela. Cette réponse est beaucoup plus concise et fiable. Merci!! – Alexander

+1

Merci de m'avoir sauvé la vie en luttant contre stacklayout: p –

+0

Cela me donne des contraintes contradictoires pour les sous-vues (taille): celles que je mets (non nulles) et celles générées par le masque autoresizing (zéro). –

Questions connexes