I\'m trying to build a titleView with constraints that looks like this:
I know how I would do
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.
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
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:
UIView
subclass, for instance CustomTitleView
that will be later used as the navigationItem
's titleView
.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).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.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
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))
);
}
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:
UIView
subclass for custom titleView
translatesAutoresizingMaskIntoConstraints
to false
for subviews and true
for titleView
itself. b) Implement sizeThatFits(size: CGSize)
titleLabel.sizeToFit()
and self.setNeedsUpdateConstraints()
inside titleView
's subclass after text changesupdateTitleView()
and make sure to call titleView.sizeToFit()
and navigationBar.setNeedsLayout()
in thereHere'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()
}
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!