Building a titleView programmatically with constraints (or generally constructing a view with constraints)

后端 未结 6 1800
长情又很酷
长情又很酷 2021-02-03 20:44

I\'m trying to build a titleView with constraints that looks like this:

\"titleView\"

I know how I would do

相关标签:
6条回答
  • 2021-02-03 21:20

    You have to set the frame of titleView because you don't specify any constraints for its position in its superview. The Auto Layout system can only figure out the size of titleView for you from the constraints you specified and the intrinsic content size of its subviews.

    0 讨论(0)
  • 2021-02-03 21:24

    Here is my implementation of 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
    
    0 讨论(0)
  • 2021-02-03 21:29

    an0's answer is correct. However, it doesn't help you getting the desired effect.

    Here's my recipe for building title views that automatically have the right size:

    • Create a UIView subclass, for instance CustomTitleView that will be later used as the navigationItem's titleView.
    • Use auto layout inside CustomTitleView. If you want to have your CustomTitleView being always centered, you'll need to add an explicit CenterX constraint (see code and link below).
    • Call updateCustomTitleView (see below) every time your titleView content updates. We need to set the titleView to nil and set it afterwards to our view again to prevent the title view being offset centered. This would happen when the title view changes from wide to narrow.
    • DON'T disable translatesAutoresizingMaskIntoConstraints

    Gist: https://gist.github.com/bhr/78758bd0bd4549f1cd1c

    Updating CustomTitleView from your 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;
    }
    

    Sample CustomTitleView.h with one label and two buttons

    #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
    

    Sample 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
    
    0 讨论(0)
  • 2021-02-03 21:31

    For combining auto-layout constraints inside titleView and hardcoded layout logic inside UINavigationBar you have to implement method sizeThatFits: inside your own custom titleView's class (subclass of UIView) like this:

    - (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 讨论(0)
  • 2021-02-03 21:37

    Thanks @Valentin Shergin and @tubtub! According to their answers I made an implementation of navigation bar title with dropdown arrow image in Swift 1.2:

    1. Create a UIView subclass for custom titleView
    2. In your subclass: a) Use auto layout for subviews but not for itself. Set translatesAutoresizingMaskIntoConstraints to false for subviews and true for titleView itself. b) Implement sizeThatFits(size: CGSize)
    3. If your title can change call titleLabel.sizeToFit() and self.setNeedsUpdateConstraints() inside titleView's subclass after text changes
    4. In your ViewController call custom updateTitleView() and make sure to call titleView.sizeToFit() and navigationBar.setNeedsLayout() in there

    Here's minimal implementation of 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)
        }
    }
    

    and 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()
    }
    
    0 讨论(0)
  • 2021-02-03 21:40

    I really needed constraints, so played around with it today. What I found that works is this:

        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
    

    Works like a charm!

    0 讨论(0)
提交回复
热议问题