Программное построение titleView с ограничениями (или, как правило, построение представления с ограничениями)

Я пытаюсь создать titleView с ограничениями, которые выглядят так:

titleView

Я знаю, как бы это сделать с фреймами. Я бы вычислил ширину текста, ширину изображения, создал бы представление с такой шириной / высотой, чтобы оно содержало и то, и другое, а затем добавило бы оба как подвиды в нужных местах с фреймами.

Я пытаюсь понять, как это можно сделать с ограничениями. Я думал, что внутренний размер контента мне поможет, но я отчаянно пытаюсь заставить это работать.

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

Мне не нужно жестко указывать размер titleView. Его можно определить по размеру содержимого, но ...

  1. TitleView определяет, что его размер равен 0, если я не закодирую фрейм жестко.
  2. Если я установлю translatesAutoresizingMaskIntoConstraints = NO, приложение выйдет из строя с этой ошибкой: 'Auto Layout still required after executing -layoutSubviews. UINavigationBar's implementation of -layoutSubviews needs to call super.'

Обновлять

Я заставил его работать с этим кодом, но мне все еще нужно установить фрейм в 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;

person Bob Spryn    schedule 08.03.2013    source источник
comment
Ваш исходный код, вероятно, не работал, потому что у вас не было ограничений по вертикальной оси; ваш второй должен быть в порядке, без необходимости устанавливать какие-либо рамки. Что произойдет, если вы создадите это представление и добавите его в другое место (а не на панель навигации, которая может внести дополнительную сложность).   -  person jrturton    schedule 21.04.2013
comment
Я пытаюсь добиться этого, не устанавливая фрейм, но я не смог найти, кто является родительским элементом представления заголовка, в который мы должны добавить ограничения ...   -  person Raphael Oliveira    schedule 03.07.2013
comment
Родителем представления является сам UINavigationBar, но по какой-то причине вам не разрешено добавлять к нему ограничения - ошибка Cannot modify constraints for UINavigationBar managed by a controller.   -  person Petar    schedule 25.11.2014


Ответы (6)


Вы должны установить фрейм titleView, потому что вы не указываете никаких ограничений для position в его супервизоре. Система Auto Layout может только вычислить size из titleView для вас только из указанных вами ограничений и intrinsic content size его подвидов.

person an0    schedule 19.04.2013
comment
Я не думаю, что это так, потому что я не менял translatesAutoresizingMaskIntoConstraints, поэтому должны были использоваться маски автоизменения размеров по умолчанию. Если бы я установил для него значение NO, он должен был бы определить размер с помощью текстовой метки и изображения внутри. - person Bob Spryn; 21.04.2013
comment
Автоматическое изменение размера требует от вас определения начального размера; Автоматическая компоновка требует, чтобы вы указали ограничение. Если вы не сделаете ни того, ни другого, ни один из них не сможет вам помочь. Подумайте об этом или просто попробуйте - установите ограничение размера или метрики для titleView, и вы увидите. - person an0; 21.04.2013
comment
Верно при автоматическом изменении размера, но даже когда я отключил translatesAutoresizingMaskIntoConstraints, мой titleView должен был иметь возможность определять его размер по дочерним элементам (метка или метка и изображение) с определенными мною ограничениями, а затем должен иметь размер. Я предполагаю, что это так, но тогда, поскольку не было никаких ограничений на размещение его в родительском элементе, он вылетел, не имея возможности определить его местоположение. - person Bob Spryn; 21.04.2013
comment
Да, как вы сами заметили, вы не указали никаких ограничений для его position в его супервизоре. Я обновил свой ответ, чтобы быть более конкретным. - person an0; 21.04.2013
comment
Итак, как указать пользовательскую позицию titleView с помощью автоматического раскладки? - person Petar; 25.11.2014
comment
@Petar см. Мой ответ ниже о том, как можно использовать автоматический макет. - person David H; 16.08.2016

Мне действительно были нужны ограничения, поэтому сегодня поигрался с ними. Я обнаружил, что работает следующее:

    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

Работает как шарм!

person David H    schedule 14.01.2016
comment
Мне жаль, что я не нашел этот ответ раньше, потому что я только что получил эту работу, хотя, используя sizeThatFits для всех подвидов, а затем вычисляя фрейм представления после этого. Этот ответ намного более краткий и надежный. Спасибо!! - person Alexander; 15.08.2016
comment
Спасибо, что спасает мне день, борясь с моим stacklayout: p - person Ayrton Werck; 26.01.2017
comment
Это дает мне противоречивые ограничения для подпредставлений (размер): те, которые я установил (ненулевые), и те, которые сгенерированы маской с автоматическим изменением размера (ноль). - person Nicolas Miari; 30.03.2017
comment
@NicolasMiari, у вас не должно быть ограничений, сгенерированных автоматически изменяющейся маской - все ограничения должны быть теми, которые вы добавили. Попробуйте начать с простого представления - может быть, ничего в нем, кроме цвета фона, а затем медленно увеличивайте его до того, что хотите. - person David H; 31.03.2017
comment
Работает как шарм, пока вы не настроите шрифты метки в traitCollectionDidChange: - person Cy-4AH; 06.12.2019
comment
@ Cy-4AH вы снова пытались использовать указанный выше код? То есть получить представление, выполнить шаги, а затем вернуть его обратно? - person David H; 06.12.2019
comment
Ярлык прыгает на панели навигации во время появления. - person Cy-4AH; 09.12.2019
comment
@ Cy-4AH создаст скелет проекта только с этим, поместит его в Dropbox или что-то подобное, просмотрит его и попытается исправить. Возможно, лучше всего будет, если вы создадите новую тему, добавите ссылку, тогда многие люди тоже могут попробовать. - person David H; 09.12.2019
comment
это не работает для меня, я пытаюсь использовать пользовательский UIView - person Ben Shabat; 22.01.2020

Ответ an0 правильный. Однако добиться желаемого эффекта это не поможет.

Вот мой рецепт создания заголовков, которые автоматически имеют правильный размер:

  • Создайте подкласс UIView, например CustomTitleView, который позже будет использоваться как titleView navigationItem.
  • Использовать автоматический макет внутри CustomTitleView. Если вы хотите, чтобы ваш CustomTitleView всегда находился по центру, вам нужно добавить явное ограничение CenterX (см. Код и ссылку ниже).
  • Звоните updateCustomTitleView (см. Ниже) каждый раз, когда ваш контент titleView обновляется. Нам нужно установить для titleView значение nil, а затем снова установить его для нашего представления, чтобы предотвратить смещение представления заголовка по центру. Это произойдет, когда вид заголовка изменится с широкого на узкий.
  • НЕ отключайте translatesAutoresizingMaskIntoConstraints

Суть: https://gist.github.com/bhr/78758bd0bd4549f1cd1c

Обновление 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;
}

Образец CustomTitleView.h с одной надписью и двумя кнопками

#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

Образец 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
person bhr    schedule 12.03.2015
comment
Спасибо, что упомянули, что не следует отключать translatesAutoresizingMaskIntoConstraints. Я установил элемент, который раньше отображался в другом месте как titleView, но после возврата он всегда будет позиционироваться неправильно. Удаление этой линии исправило это. - person Livven; 11.08.2016
comment
Мое представление заголовка отображается в центре, бит его подпредставления отображается в точке (0,0) в координатах окна (самый верхний, крайний левый). Я делаю в основном то же, что и вы, за исключением использования API привязок для ограничений. - person Nicolas Miari; 30.03.2017

Спасибо @Valentin Shergin и @tubtub! Согласно их ответам я реализовал заголовок панели навигации с изображением стрелки раскрывающегося списка в Swift 1.2:

  1. Создайте подкласс UIView для пользовательского titleView
  2. В вашем подклассе: a) Используйте автоматический макет для подвидов, но не для себя. Установите translatesAutoresizingMaskIntoConstraints на false для вложенных просмотров и true для самого titleView. б) Реализовать sizeThatFits(size: CGSize)
  3. Если ваш заголовок может измениться, вызовите titleLabel.sizeToFit() и self.setNeedsUpdateConstraints() внутри подкласса titleView после изменения текста
  4. В вашем ViewController вызовите custom updateTitleView() и обязательно вызовите titleView.sizeToFit() и navigationBar.setNeedsLayout() там

Вот минимальная реализация 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)
    }
}

и 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()
}
person Lion    schedule 27.09.2015
comment
Не могу заставить его работать. Связанные прямоугольники, вычисленные в пределах sizeThatFits(), возвращают ноль для ширины и высоты. - person Nicolas Miari; 30.03.2017

Для объединения ограничений автоматической компоновки внутри titleView и жестко запрограммированной логики компоновки внутри UINavigationBar вы должны реализовать метод sizeThatFits: внутри вашего собственного пользовательского класса titleView (подкласс UIView) следующим образом:

- (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))
    );
}
person Valentin Shergin    schedule 10.05.2015

Вот моя реализация ImageAndTextView

@interface ImageAndTextView()
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UITextField *textField;
@end

@implementation ImageAndTextView

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        [self initializeView];
    }

    return self;
}

- (void)initializeView
{
    self.translatesAutoresizingMaskIntoConstraints = YES;
    self.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);

    self.imageView = [[UIImageView alloc] init];
    self.imageView.contentMode = UIViewContentModeScaleAspectFit;
    self.textField = [[UITextField alloc] init];
    [self addSubview:self.imageView];
    [self addSubview:self.textField];

    self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
    self.textField.translatesAutoresizingMaskIntoConstraints = NO;
    //Center the text field
    [NSLayoutConstraint activateConstraints:@[
        [self.textField.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
        [self.textField.centerYAnchor constraintEqualToAnchor:self.centerYAnchor]
    ]];

    //Put image view on left of text field
    [NSLayoutConstraint activateConstraints:@[
        [self.imageView.rightAnchor constraintEqualToAnchor:self.textField.leftAnchor],
        [self.imageView.lastBaselineAnchor constraintEqualToAnchor:self.textField.lastBaselineAnchor],
        [self.imageView.heightAnchor constraintEqualToConstant:16]
    ]];
}

- (CGSize)intrinsicContentSize
{
    return CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
}
@end
person Omkar    schedule 29.10.2019