17

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;
}
Community
  • 1
  • 1
emenegro
  • 6,901
  • 10
  • 45
  • 68
  • 1
    I think UIKit and Core Text layout a bit differently... A quick fix would be to just use attributed string in both labels, I guess – Lescai Ionel Aug 23 '13 at 12:18
  • Yes, I thought on that to be the solution but I would prefer to solve this using those approaches indifferently. – emenegro Aug 26 '13 at 06:51
  • That looks like a UIKit bug. There's not much to be done but to file it. – Ken Aug 26 '13 at 17:51
  • I had a similar problem with labels with different size. http://stackoverflow.com/questions/9910766/how-to-align-baselines-of-text-in-uilabels-with-different-font-sizes-on-ios – Raphael Oliveira Aug 28 '13 at 11:07
  • really, I'm not able to reproduce the problem. The labels are correctly aligned in both cases. Can you post more code or an example project? I'll check for a workaround – LombaX Sep 02 '13 at 09:44
  • @LombaX, I don't know what is going on. I made a test project and it works fine. The only difference between the test project and my code is I have my code in a cell, and the test project is a regular view. Could it be that? – emenegro Sep 03 '13 at 07:26

1 Answers1

12

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

Community
  • 1
  • 1
LombaX
  • 17,265
  • 5
  • 52
  • 77
  • But that seems really strange. What is the puspose of auto layout then? Do I have to calculate things about fonts innards like we had to do in the past with spring & struts? It seems really really awkward. – emenegro Sep 03 '13 at 09:41
  • Autolayout is working correctly, since the bounds of the views are aligned. The purpose of autolayout is to layout the components, and in this case the component is the UILabel, not the content of the UILabel. Moreover, if you expect to have a similar feature on a UILabel as a standard, how would you implement it? Since UILabel supports multiple lines, how would you align the baselines of a multiline lable with two different font sizes in "a standard way"? – LombaX Sep 03 '13 at 12:46
  • Maybe I totally misunderstood what is baseline alignment but I thought "NSLayoutFormatAlignAllBaseline – This lines up the views according to their baseline, which for text based views is the bottom of text that does not drop down (like g, p, j, etc). For non-text views it is the same as the bottom edge". – emenegro Sep 03 '13 at 15:23
  • `NSLayoutFormatAlignAllBaseline` is meant to work together with `viewForBaselineLayout`, and should do exactly what you are looking for. But, unfortunately, it's not implemented by UILabel (it uses the default implementation, returning self, and not the subview for the baseline alignment). I don't think this is a bug, but expected behavior since for the reason I explained in the previous comment. – LombaX Sep 03 '13 at 15:55
  • Ok, finally I understand... well, this is totally disappointing. What do you think is the better solution to avoid this? I'm thinking on subclassing `UILabel` (I've already did that for other purposes) to return the correct `viewForBaselineLayout` taking into account the calculations using the descender and the frame. I'm only writing my thoughts, I have to test if it is possible... Thank you so much for your help, LombaX. – emenegro Sep 03 '13 at 16:08
  • Something similar, yes. It's possible, but you have to add a real view as a subview of the subclassed UILabel. I've tried in a sample project and it works. Give me a moment and I'll update the answer – LombaX Sep 04 '13 at 07:25
  • Perfect, it works like a charm. Thank you so much for your help :D I've updated my project with your solution, but keeping the four labels (I had to adjust some constraint priorities to achieve it). – emenegro Sep 04 '13 at 10:04
  • If you want, take a look to the updated project and the constraints and priorities, this way it seems to works without taking into account your advices above. – emenegro Sep 04 '13 at 10:18
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/36776/discussion-between-lombax-and-emenegro) – LombaX Sep 04 '13 at 10:48
  • It seems as if iOS 7 does the correct base alignment for labels by default, but you still need the workaround for iOS 6. – fabb Feb 28 '14 at 06:55
  • I'm seeing incorrect alignment in iOS 12.1 / Xcode 10.1. – Greg Apr 11 '19 at 19:23