使用约束编程构建titleView(或一般地使用约束构建视图)

32

我正在尝试使用约束构建一个标题视图,它看起来像这样:

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

我不应该硬编码标题视图的大小。它应该能够通过其内容的大小来确定,但是...

  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;

你的原始代码可能无法正常工作,因为在垂直轴上没有任何约束条件;你的第二个代码应该可以正常工作,而不需要设置任何框架。如果你将该视图创建并添加到其他位置(而不是导航栏中,这可能会引入额外的复杂性),会发生什么? - jrturton
1
我正在尝试在不设置框架的情况下实现这一点,但我找不到标题视图的父级,我们应该在其中添加约束... - Raphael Oliveira
1
视图的父视图是 UINavigationBar 本身 - 但由于某些原因,您不能向其添加约束 - 失败并显示 Cannot modify constraints for UINavigationBar managed by a controller - Petar
6个回答

71

我真的需要约束,所以今天试着玩了一下。我发现这样做是有效的:

    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

运作得像魔术一样!


2
我希望早点找到这个答案,因为我刚刚让它工作了,虽然是通过对所有子视图使用sizeThatFits,然后计算视图的框架。这个答案更加简洁可靠。谢谢! - Alexander
1
谢谢,这救了我的一天,我正在与我的StackLayout挣扎 :p - Ayrton Werck
1
@NicolasMiari您应该没有“autoresizing mask-generated ones”约束 - 所有约束都应该是您添加的。尝试从一个简单的视图开始 - 也许除了背景颜色什么也没有,然后慢慢构建它到您想要的样子。 - David H
标签在导航栏出现时跳动。 - Cy-4AH
@Cy-4AH 创建一个骨架项目,只需这样做,将其放在Dropbox或类似的平台上,我会查看并尝试修复它。也许最好是创建一个新主题,添加链接,那么很多人也可以尝试。 - David H
显示剩余4条评论

19

an0的回答是正确的,但它并不能帮助您获得所需的效果。

以下是我创建自适应大小标题视图的步骤:

  • 创建一个UIView子类,例如CustomTitleView,稍后将用作navigationItemtitleView
  • CustomTitleView内使用自动布局。如果您想始终居中您的CustomTitleView,则需要添加一个明确的CenterX约束(请参见下面的代码和链接)。
  • 每次titleView内容更新时调用updateCustomTitleView(请参见下方)。我们需要将titleView设置为nil,然后再次将其设置为我们的视图,以防止标题视图偏移到中心。当标题视图从宽变窄时,会发生这种情况。
  • 不要禁用translatesAutoresizingMaskIntoConstraints

代码片段:https://gist.github.com/bhr/78758bd0bd4549f1cd1c

从您的ViewController更新CustomTitleView

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

谢谢提醒,不应该禁用 translatesAutoresizingMaskIntoConstraints。我将一个先前在其他位置显示的元素设置为 titleView,但在导航回来后它总是定位不正确。删除那行代码解决了问题。 - Livven
我的标题视图显示在中心,但其子视图在窗口坐标(最上面,最左边)的(0,0)处出现。我基本上做的和你一样,只是使用了锚点API来进行约束。 - Nicolas Miari

10

感谢 @Valentin Shergin 和 @tubtub 的回答!根据他们的答案,我在 Swift 1.2 中实现了带有下拉箭头图像的导航栏标题:

  1. 创建一个 UIView 子类,用于自定义 titleView
  2. 在您的子类中:a)对子视图使用自动布局,但对自身不使用。为子视图设置 translatesAutoresizingMaskIntoConstraintsfalse,对于 titleView 自身则为 true。b)实现 sizeThatFits(size: CGSize)
  3. 如果您的标题可能会更改,请在文本更改后,在 titleView 的子类中调用 titleLabel.sizeToFit()self.setNeedsUpdateConstraints()
  4. 在您的 ViewController 中调用自定义的 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()
}

1
无法使其正常工作。在sizeThatFits()中计算的边界矩形返回宽度和高度为零。 - Nicolas Miari

8

你需要设置titleView的框架,因为你没有在其父视图中指定任何位置的约束条件。自动布局系统只能从你指定的约束条件和其子视图的内在内容大小来计算出titleView大小


我认为情况并非如此,因为我没有改变“translatesAutoresizingMaskIntoConstraints”,所以它应该一直使用默认的自动调整掩码。如果我将其设置为“NO”,那么它应该能够通过文本标签和内部图像来确定大小。 - Bob Spryn
自动调整大小需要您定义初始大小;自动布局需要您指定约束条件。如果您两者都不做,它们都无法帮助您。想一想,或者试试吧 - 为 titleView 设置大小或度量约束条件,您就会明白。 - an0
即使我禁用了 translatesAutoresizingMaskIntoConstraints,启用自动调整大小功能仍然为 True,但是我的 titleView 应该能够根据我定义的约束条件从其子元素(标签或标签和图像)确定其大小,然后应该有一个大小。 我猜它确实这样做了,但是由于没有将其放置在父级中的约束条件,它会崩溃并无法确定其位置。 - Bob Spryn
是的,正如您自己注意到的那样,在其父视图中没有为其“位置”指定任何约束条件。我更新了我的答案以更具体地说明。 - an0
4
如何使用自动布局来指定自定义titleView的位置? - Petar
@Petar,看下面我的答案,你可以了解如何使用自动布局。 - David H

6

如果要在titleView中组合自动布局约束和硬编码布局逻辑,您需要在自定义的titleView类(UIView子类)中实现sizeThatFits:方法,如下所示:

- (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))
    );
}

0

这是我的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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接