Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UILabel + AutoLayout = error in baseline alignment

I'm having problem while aligning two labels. Two examples to illustrate the problem

Example 1 (OK)

[leftLabel setText:@"03"];
[rightLabel setText:@"Description3"];

enter image description here

Example 2 (NOK)

[leftLabel setText:@"03"];
[rightLabel setAttributedText:[[NSAttributedString alloc] initWithString:@"Description3"]];

enter image description here

In both examples the layout constraint is this

[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-bigMargin-[leftLabel]-bigMargin-[rightLabel]-bigMargin-|"
                                        options:NSLayoutFormatAlignAllBaseline
                                        metrics:metrics
                                          views:views];

The problem is the right label, when the text is an attributed one it is drawn one point below, as seen in the images, and the alignment results wrong.

Why? Can I solve this using UIlabel and both approaches?

EDIT:

I've created a project in GitHub with a test on this. The question here is I'm having the issue even without NSAttributdString! Look at the label with the number, is not correctly aligned with the description and amount.

enter image description here

I paste here the code of the cell but the whole scenario must be seen in the project.

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        
        UIView *contentView = [self contentView];
        
        [contentView setBackgroundColor:[UIColor clearColor]];
        
        dayLabel_ = [[UILabel alloc] initWithFrame:CGRectZero];
        [dayLabel_ setTranslatesAutoresizingMaskIntoConstraints:NO];
        [contentView addSubview:dayLabel_];
        
        monthLabel_ = [[UILabel alloc] initWithFrame:CGRectZero];
        [monthLabel_ setTranslatesAutoresizingMaskIntoConstraints:NO];
        [monthLabel_ setFont:[UIFont boldSystemFontOfSize:13.0f]];
        [contentView addSubview:monthLabel_];
        
        descriptionLabel_ = [[UILabel alloc] initWithFrame:CGRectZero];
        [descriptionLabel_ setTranslatesAutoresizingMaskIntoConstraints:NO];
        [descriptionLabel_ setFont:[UIFont systemFontOfSize:20.0f]];
        [contentView addSubview:descriptionLabel_];
        
        conceptLabel_ = [[UILabel alloc] initWithFrame:CGRectZero];
        [conceptLabel_ setTranslatesAutoresizingMaskIntoConstraints:NO];
        [conceptLabel_ setLineBreakMode:NSLineBreakByTruncatingTail];
        [conceptLabel_ setFont:[UIFont systemFontOfSize:12.0f]];
        [contentView addSubview:conceptLabel_];
        
        amountLabel_ = [[UILabel alloc] initWithFrame:CGRectZero];
        [amountLabel_ setTranslatesAutoresizingMaskIntoConstraints:NO];
        [contentView addSubview:amountLabel_];
        
        // Constraints
        
        NSDictionary *views = NSDictionaryOfVariableBindings(contentView, dayLabel_, monthLabel_, descriptionLabel_, conceptLabel_, amountLabel_);
        NSDictionary *metrics = @{ @"bigMargin" : @12 };
        
        [descriptionLabel_ setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
        [conceptLabel_ setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
                
        [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-bigMargin-[dayLabel_][monthLabel_]"
                                                                            options:NSLayoutFormatAlignAllLeading
                                                                            metrics:metrics
                                                                              views:views]];
        
        [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-bigMargin-[descriptionLabel_][conceptLabel_]"
                                                                            options:NSLayoutFormatAlignAllLeading
                                                                            metrics:metrics
                                                                              views:views]];
        
        [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-bigMargin-[dayLabel_]-bigMargin-[descriptionLabel_]-(>=bigMargin)-[amountLabel_]-bigMargin-|"
                                                                            options:NSLayoutFormatAlignAllBaseline
                                                                            metrics:metrics
                                                                              views:views]];
        
        [contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-bigMargin-[monthLabel_]-bigMargin-[conceptLabel_]-bigMargin-|"
                                                                            options:NSLayoutFormatAlignAllBaseline
                                                                            metrics:metrics
                                                                              views:views]];
    }
    
    return self;
}
like image 476
emenegro Avatar asked Aug 23 '13 12:08

emenegro


1 Answers

Ok, with the last example the problem is more clear. The first thing you must know: the text inside an UILabel is, by default, vertically aligned at the center of the label.
AFAIK you can't change the vertical align to have the text aligned to the base line.

Now, look at the attached image.

Comparison

In the first example, I leaved all the default values of your sample project. You can see that the day label and the description label are perfectly aligned: Autolayout aligns the bounds of the two labels (that are simply views, with other private subviews inside).
But, the font size is different. For the day label is the default system size (17), for the description you specified 20.
Now, if the two labels are aligned, the text is vertically center in the label, and the font size is different, obviously the baseline of the two text won't be aligned.

In the second example, I used the same font size, and you can see that the alignment is correct.

So, the possible solutions are two:

  • use the same font (with the same size) in all the labels
  • if you want to use fonts of different kind / size, you must change the position / size of the labels to have the baseline correctly aligned

This last point can be done with some calculation, and you can find some example here: Link

EDIT

Ok, I post an example. First thing: it seems that, at the link posted, there is an error. ascender + descender + 1 is equal to the lineHeight, and not to the pointSize. I asked the author to correct it.

So, you can subclass UILabel, override viewForBaselineLayout and do something similar. You have to add a baselineView instance variable and add it as a subview of the UILabel, since AutoLayout want the view to align to be a subview of the label.

// please note: you may need some error checking
- (UIView *)viewForBaselineLayout
{
    // create the view if not exists, start with rect zero
    if (!self.baselineView)
    {
        self.baselineView = [[UIView alloc] initWithFrame:CGRectZero];
        self.baselineView.backgroundColor = [UIColor clearColor];
        [self addSubview:self.baselineView];
    }

    // this is the total height of the label
    float viewHeight = self.bounds.size.height;

    // calculate the space that is above the text
    float spaceAboveText = (viewHeight - self.font.lineHeight) / 2;

    // this is the height of the view we want to align to
    float baselineViewHeight = spaceAboveText + self.font.ascender + 1;

    // if you have 26.6545 (for example), the view takes 26.0 for the height. This is not good, so we have to round the number correctly (to the upper value if >.5 and to the lower if <.5)
    int integerBaseline = (int)(baselineViewHeight + 0.5f);

    // update the frame of the view
    self.baselineView.frame = CGRectMake(0, 0, self.bounds.size.width, (float)integerBaseline);


    return self.baselineView;

}

With this approach you have to take care of few things:

  • the label must have a size at the time autolayout calls viewForBaselineLayout, and its height must not change after. So you have to fit the label to it's content size before the layout process takes place.
  • you have adjust your constraints according to the above advice (now you are fixing, for example, dayLabel with monthLabel, but you can't since dayLabel must "float" now).

I attach your project with some updates and colors for debug, only with dayLabel and descriptionLabel (I removed the others) and with updated contraints.

Wrong Layout Updated

like image 86
LombaX Avatar answered Nov 07 '22 02:11

LombaX