Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

titleView

I know how I would do this with frames. I would calculate the width of the text, the width of the image, create a view with that width/height to contain both, then add both as subviews at the proper locations with frames.

I'm trying to understand how one might do this with constraints. My thought was that intrinsic content size would help me out here, but I'm flailing around wildly trying to get this to work.

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 

I shouldn't have to hardcode the size of the titleView. It should be able to be determined via the size of its contents, but...

  1. The titleView is determining it's size is 0 unless I hardcode a frame.
  2. If I set translatesAutoresizingMaskIntoConstraints = NO the app crashes with this error: 'Auto Layout still required after executing -layoutSubviews. UINavigationBar's implementation of -layoutSubviews needs to call super.'

Update

I got it to work with this code, but I'm still having to set the frame on the 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; 
like image 207
Bob Spryn Avatar asked Mar 08 '13 02:03

Bob Spryn


2 Answers

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!

like image 103
David H Avatar answered Oct 02 '22 09:10

David H


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 
like image 39
bhr Avatar answered Oct 02 '22 08:10

bhr