以编程方式构建带有约束的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
,稍后将用作navigationItem
的titleView
。
在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
中为你找出titleView
的size
。
【讨论】:
我不这么认为,因为我没有更改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(或通常构建带有约束的视图)的主要内容,如果未能解决你的问题,请参考以下文章