Подтвердить что ты не робот

Создание 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. Он должен быть определен по размеру его содержимого, но...

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

Update

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

Ответ 1

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

Ответ 2

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

    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

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

Ответ 3

an0 ответ правильный. Однако это не поможет вам получить желаемый эффект.

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

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

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

Ответ 4

Спасибо @Валентин Шергин и @tubtub! Согласно их ответам, я сделал реализацию заголовка навигационной панели со стрелкой вниз в Swift 1.2:

  • Создайте подкласс UIView для пользовательских titleView
  • В вашем подклассе: a) Используйте автомасштабирование для подсмотров, но не для себя. Установите translatesAutoresizingMaskIntoConstraints в false для субвью и true для titleView. b) Внедрить sizeThatFits(size: CGSize)
  • Если ваш заголовок может изменить вызов titleLabel.sizeToFit() и self.setNeedsUpdateConstraints() внутри подкласса titleView после изменения текста
  • В вашем вызове 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()
}

Ответ 5

Для комбинирования ограничений автоматической компоновки внутри 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))
    );
}