Создание 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;
Ответы
Ответ 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))
);
}