使用约束以编程方式构建titleView(或者通常使用约束构建视图)

时间:2013-03-08 02:30:14

标签: ios nslayoutconstraint

我正在尝试使用如下所示的约束构建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的大小。它应该能够通过其内容的大小来确定,但是......

  1. titleView确定它的大小为0,除非我对帧进行硬编码。
  2. 如果我设置translatesAutoresizingMaskIntoConstraints = NO应用程序崩溃时出现此错误:'Auto Layout still required after executing -layoutSubviews. UINavigationBar's implementation of -layoutSubviews needs to call super.'
  3. 更新

    我让它使用这段代码,但我仍然需要在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;
    

6 个答案:

答案 0 :(得分:59)

我真的需要限制,所以今天就玩了。我发现有效的是:

    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

像魅力一样!

答案 1 :(得分:18)

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

答案 2 :(得分:8)

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

  1. 为自定义UIView
  2. 创建titleView子类
  3. 在您的子类中:a)对子视图使用自动布局,但不为自己使用。为子视图设置translatesAutoresizingMaskIntoConstraintsfalse,为true设置titleView。 b)实施sizeThatFits(size: CGSize)
  4. 如果您的标题可以在文字更改后更改titleLabel.sizeToFit()self.setNeedsUpdateConstraints()内的titleView子类,则
  5. 在您的ViewController调用自定义updateTitleView()并确保在其中调用titleView.sizeToFit()navigationBar.setNeedsLayout()
  6. 这是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()
    }
    

答案 3 :(得分:7)

您必须设置titleView的框架,因为您没有在其超级视图中为其position指定任何约束。自动布局系统只能根据您指定的约束和其子视图的size为您找出titleView intrinsic content size

答案 4 :(得分:5)

要将titleView内的自动布局约束与UINavigationBar内的硬编码布局逻辑相结合,您必须在自己的自定义sizeThatFits:类中实现方法titleViewUIView的子类)像这样:

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

答案 5 :(得分: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