以编程方式构建带有约束的titleView(或通常构建带有约束的视图)

Posted

技术标签:

【中文标题】以编程方式构建带有约束的titleView(或通常构建带有约束的视图)【英文标题】:Building a titleView programmatically with constraints (or generally constructing a view with constraints) 【发布时间】:2013-03-08 02:30:14 【问题描述】:

我正在尝试构建一个带有如下约束的 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'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;

【问题讨论】:

您的原始代码可能不起作用,因为您在垂直轴上没有任何约束;您的第二个应该没问题,而无需设置任何框架。如果您创建该视图并将其添加到其他位置(而不是导航栏,这可能会引入额外的复杂性)会发生什么。 我试图在不设置框架的情况下实现这一点,但我找不到我们应该在其中添加约束的标题视图的父级... 视图的父级是 UINavigationBar 本身 - 但由于某种原因,您不能向其添加约束 - 以 Cannot modify constraints for UINavigationBar managed by a controller 失败。 【参考方案1】:

我真的需要约束,所以今天玩弄了它。我发现有效的是:

    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

像魅力一样工作!

【讨论】:

我希望我早点找到这个答案,因为我刚刚开始工作,尽管对所有子视图使用sizeThatFits,然后计算视图的框架。这个答案更加简洁可靠。谢谢!! 感谢它节省了我的一天,为我的 stacklayout 苦苦挣扎:p 这给了我对子视图(大小)的冲突约束:我设置的约束(非零)和自动调整大小的掩码生成的约束(零)。 @NicolasMiari 你应该没有“自动调整掩码生成的”约束 - 所有约束都应该是你添加的约束。尝试从一个简单的视图开始——可能除了背景颜色之外什么都没有,然后慢慢将它构建成你想要的。 在您在traitCollectionDidChange: 中配置标签字体之前就像一个魅力一样工作【参考方案2】:

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,但在导航回来后它总是定位不正确。删除该行修复它。 我的标题视图显示在中心,其子视图位出现在窗口坐标(最顶部,最左侧)的 (0,0) 处。我和你做的基本一样,除了使用锚 API 进行约束。【参考方案3】:

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

    为自定义titleView 创建一个UIView 子类 在您的子类中: a) 对子视图使用自动布局,但对其自身不使用。将translatesAutoresizingMaskIntoConstraints 设置为false 用于子视图,将true 设置为titleView 本身。 b) 实施sizeThatFits(size: CGSize) 如果您的标题可以在文本更改后在titleView 的子类中更改调用titleLabel.sizeToFit()self.setNeedsUpdateConstraints() 在您的 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)
    

和视图控制器:

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

【讨论】:

无法让它工作。在sizeThatFits() 内计算的绑定矩形的宽度和高度返回零。【参考方案4】:

您必须设置titleView 的框架,因为您没有在其父视图中为其position 指定任何约束。 Auto Layout 系统只能从你指定的约束和它的子视图的intrinsic content size 中为你找出titleViewsize

【讨论】:

我不这么认为,因为我没有更改translatesAutoresizingMaskIntoConstraints,所以它应该使用默认的自动调整大小掩码。如果我确实将其设置为NO,那么它应该能够通过里面的文本标签和图像来确定大小。 自动调整大小需要你定义初始大小;自动布局需要您指定约束。如果你都不做,他们都不能帮助你。想一想,或者只是尝试一下——为titleView 设置大小或度量约束,你会看到的。 True 自动调整大小,但即使我禁用 translatesAutoresizingMaskIntoConstraints,我的 titleView 也应该能够根据我定义的约束从它的子项(标签或标签和图像)确定它的大小,然后应该有大小。我猜是这样,但是由于没有限制将其定位在父级中,因此无法确定其位置会崩溃。 是的,正如您自己注意到的那样,您没有在其父视图中为其position 指定任何约束。我更新了我的答案以更具体。 那么如何使用自动布局指定自定义titleView的位置?【参考方案5】:

要在titleView 中结合自动布局约束和UINavigationBar 中的硬编码布局逻辑,您必须在您自己的自定义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))
    );

【讨论】:

【参考方案6】:

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

【讨论】:

以上是关于以编程方式构建带有约束的titleView(或通常构建带有约束的视图)的主要内容,如果未能解决你的问题,请参考以下文章

以编程方式更改约束只会影响视图的可见部分

如何以编程方式创建带有约束的视图

目标 C:以编程方式添加带有约束的 UI 按钮

ObjC,在以编程方式添加/删除后恢复为界面构建器自动布局约束?

以编程方式快速更改纵横比约束

以编程方式添加约束会破坏自动布局约束