2013-03-08 13 views
24

Próbuję zbudować titleView z ograniczeniami, który wygląda tak:Budowanie titleView programowo z ograniczeniami (lub ogólnie konstruowania widok z ograniczeniami)

titleView

wiem, jak bym to zrobić ramki. Chciałbym obliczyć szerokość tekstu, szerokość obrazu, utworzyć widok o tej szerokości/wysokości, aby zawierał oba, a następnie dodać oba jako subviews w odpowiednich miejscach z ramkami.

Próbuję zrozumieć, jak można to zrobić z ograniczeniami. Pomyślałem, że wewnętrzny rozmiar treści pomógłby mi tutaj, ale ja wymiotuję dookoła, próbując uruchomić to.

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 

Nie powinieneś wymagać twardego kodu rozmiaru titleView. Powinno być możliwe do określenia poprzez rozmiar jego zawartości, ale ...

  1. TitleView określa, że ​​jego rozmiar to 0, chyba że utknę w ramkę.
  2. Jeżeli ustawić translatesAutoresizingMaskIntoConstraints = NO Aplikacja ulega awarii z tego błędu: 'Auto Layout still required after executing -layoutSubviews. UINavigationBar's implementation of -layoutSubviews needs to call super.'

aktualizacji

Dostałem go do pracy z tym kodem, ale ciągle mam ustawić ramkę na 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

Twój oryginalny kod prawdopodobnie nie działał, ponieważ nie masz żadnych ograniczeń w osi pionowej; twoja druga powinna wystarczyć bez ustawiania żadnych ramek. Co się stanie, jeśli utworzysz ten widok i dodasz go gdzie indziej (zamiast do paska nawigacji, który może wprowadzać dodatkową złożoność). – jrturton

+1

Próbuję osiągnąć to bez ustawiania ramki, ale nie mogłem znaleźć, kto jest rodzicem widoku tytułu, w którym powinniśmy dodać ograniczenia ... –

+1

Rodzicem widoku jest sam 'UINavigationBar' - ale jesteś niedozwolone dodawanie do niego ograniczeń z jakiegoś powodu - niepowodzenie z 'Nie można zmodyfikować ograniczeń dla UINavigationBar zarządzanego przez kontroler'. – Petar

Odpowiedz

7

Musisz ustawić ramkę titleView, ponieważ nie określasz żadnych ograniczeń dla jej position w swoim superzokurze. System Auto Layout może znaleźć dla ciebie tylko size z określonych więzów i intrinsic content size swoich pod-widoków.

+0

Nie sądzę, że tak jest, ponieważ nie zmieniłem 'translatesAutoresizingMaskIntoConstraints', więc powinno używać domyślnych maskowania automatycznego. Jeśli ustawiłem to na "NIE", powinno być możliwe określenie rozmiaru za pomocą etykiety i obrazu wewnątrz. –

+0

Automatyczne zmiany rozmiaru wymagają określenia początkowego rozmiaru; Auto Layout wymaga określenia ograniczenia. Jeśli nie zrobisz żadnej, żadna z nich nie może ci pomóc. Pomyśl o tym lub po prostu spróbuj - ustaw ograniczenie rozmiaru lub danych dla 'titleView', a zobaczysz. – an0

+0

Prawda przy automatycznej zmianie rozmiaru, ale nawet po wyłączeniu 'translatesAutoresizingMaskIntoConstraints', mój titleView powinien był w stanie określić jego rozmiar z jego dzieci (etykiety lub etykiety i obrazu) z ograniczeniami zdefiniowałem, a następnie powinien był mieć rozmiar. Zgaduję, że tak, ale potem, ponieważ nie było żadnych ograniczeń pozycjonujących go w rodzicu, spowodowałoby to brak możliwości określenia jego położenia. –

18

Odpowiedź an0 jest poprawna. Jednak nie pomaga uzyskać pożądanego efektu.

Oto mój przepis na budowę widoki tytuł, który automatycznie mają prawo rozmiar:

  • Utwórz UIView podklasy, na przykład CustomTitleView które zostaną później wykorzystane jako navigationItem „s titleView.
  • Użyj automatycznego układu wewnątrz CustomTitleView. Jeśli chcesz, aby Twój CustomTitleView był zawsze wyśrodkowany, musisz dodać wyraźne ograniczenie dla CenterX (patrz kod i link poniżej).
  • Zadzwoń pod updateCustomTitleView (patrz poniżej) za każdym razem, gdy aktualizujesz zawartość titleView. Musimy ustawić titleView na zero i następnie ustawić go ponownie na naszym widoku, aby zapobiec wyrównaniu wyrównania widoku tytułu. Stanie się tak, gdy widok tytułu zmieni się z szerokiego na wąski.
  • NIE wyłączyć translatesAutoresizingMaskIntoConstraints

Streszczenie: https://gist.github.com/bhr/78758bd0bd4549f1cd1c

Aktualizacja CustomTitleView z 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; 
} 

Sample CustomTitleView.h jedną etykietę i dwa przyciski

#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 

Próbka 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

Dziękuję za wzmiankę, że nie należy wyłączać 'translatesAutoresizingMaskIntoConstraints'. Ustawiłem element, który był wcześniej pokazywany w innym miejscu jako "titleView", ale zawsze będzie ustawiony nieprawidłowo po przejściu z powrotem. Usunięcie tej linii naprawiło to. – Livven

+0

Mój widok tytułowy jest wyświetlany w środku, a jego podobrazie pojawiają się na (0,0) we współrzędnych okna (najwyżej, po lewej stronie). Zasadniczo robię to samo, co ty, z wyjątkiem zastosowania API kotwicy dla więzów. –

5

Do łączenia ograniczenia automatycznego układu wewnątrz titleView i zakodowanego na stałe logiki układu wewnątrz UINavigationBar trzeba zaimplementować metodę sizeThatFits: wewnątrz własnego niestandardowego titleView „s klasa (podklasa UIView) tak:

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

Dzięki @Valentin Shergin and @tubtub! Według ich odpowiedzi zrobiłem realizację tytułu paska nawigacji z rozwijanego strzałką obrazu w Swift 1.2:

  1. Tworzenie UIView podklasy dla zwyczaju titleView
  2. W swojej podklasy: a) stosowanie układu automatycznego dla subviews ale nie dla samo. Ustaw translatesAutoresizingMaskIntoConstraints na false dla subviews i true dla samej siebie. b) Wdrożenie sizeThatFits(size: CGSize)
  3. Jeśli tytuł może zmienić rozmowę titleLabel.sizeToFit() i self.setNeedsUpdateConstraints() wewnątrz podklasy titleView „s po tekst zmienia
  4. W niestandardowym updateTitleView() rozmowy ViewController i upewnij się, aby zadzwonić titleView.sizeToFit() i navigationBar.setNeedsLayout() tam

Oto minimalna realizacja 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) 
    } 
} 

i 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

Nie można go uruchomić. Związane recty obliczane w ramach 'sizeThatFits()' zwracają zero dla szerokości i wysokości. –

47

Naprawdę potrzebowałem ograniczeń, więc bawiłem się z nim dzisiaj. To, co znalazłem, działa:

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 

Działa jak urok!

+2

Życzę sobie znaleźć tę odpowiedź wcześniej, ponieważ właśnie to działa, chociaż używając "sizeThatFits" dla wszystkich subviews, a następnie obliczając ramkę widoku po tym. Ta odpowiedź jest dużo bardziej zwięzła i wiarygodna. Dzięki!! – Alexander

+1

Dzięki temu oszczędzam mój dzień, borykając się z moim stosem: p –

+0

Daje mi to sprzeczne ograniczenia dla subskrybentów (rozmiaru): te, które ustawiłem (niezerowe) i te, które generują maski automatyczne (zero). –