Top-aligning text of different sizes within a UILabel
That's too bad. I don't have enough reputation to comment the answer that I used well after a little bit modification.
I just want to point out that use smallfont
for cents instead of font
, which might be a typo.
Below is the modified code
- (NSMutableAttributedString *)styleSalePriceLabel:(NSString *)salePrice withFont:(UIFont *)font
{
if ([salePrice rangeOfString:@"."].location == NSNotFound) {
return [[NSMutableAttributedString alloc]
initWithString:salePrice];
} else {
NSRange range = [salePrice rangeOfString:@"."];
range.length = (salePrice.length - range.location);
NSMutableAttributedString *stylizedPriceLabel =
[[NSMutableAttributedString alloc] initWithString:salePrice];
UIFont *smallfont = [UIFont fontWithName:font.fontName
size:(font.pointSize / 2)];
NSNumber *offsetAmount = @(font.capHeight - smallfont.capHeight);
[stylizedPriceLabel addAttribute:NSFontAttributeName
value:smallfont
range:range];
[stylizedPriceLabel addAttribute:NSBaselineOffsetAttributeName
value:offsetAmount
range:range];
return stylizedPriceLabel;
}
}
The problem with trying to do this simply by aligning the origins of frames is that "normal" characters usually end up with some extra padding around them because the label must accommodate all of the font's characters, including ones will tall ascenders and long descenders. You'll notice in the image you posted that if the smaller "99" were a separate label that was set to the same origin as the bigger text, it would be too high because of the dollar sign's top-most point.
Fortunately, UIFont
gives us all the information we need to do this properly. We need to measure the empty ascender space the labels are using and adjust the relative positioning to account for it, like so:
//Make sure the labels hug their contents
[self.bigTextLabel sizeToFit];
[self.smallTextLabel sizeToFit];
//Figure out the "blank" space above normal character height for the big text
UIFont *bigFont = self.bigTextLabel.font;
CGFloat bigAscenderSpace = (bigFont.ascender - bigFont.capHeight);
//Move the small text down by that ammount
CGFloat smallTextOrigin = CGRectGetMinY(self.bigTextLabel.frame) + bigAscenderSpace;
//Figure out the "blank" space above normal character height for the little text
UIFont *smallFont = self.smallTextLabel.font;
CGFloat smallAscenderSpace = smallFont.ascender - smallFont.capHeight;
//Move the small text back up by that ammount
smallTextOrigin -= smallAscenderSpace;
//Actually assign the frames
CGRect smallTextFrame = self.smallTextLabel.frame;
smallTextFrame.origin.y = smallTextOrigin;
self.smallTextLabel.frame = smallTextFrame;
(This code assumes you have two label properties named bigTextLabel
and smallTextLabel
, respectively)
Edit:
Doing this without two labels is fairly similar. You can make a custom UIView subclass and draw NSAttributedStrings
in it with the -drawInRect:options:context:
method (make sure to use NSStringDrawingUsesLineFragmentOrigin
in your options). The math for working out the top alignment should be the same, the only difference being you get the font from the attributed string via the NSFontAttributeName
attribute, not the label. The 2012 WWDC video on attributed string drawing is a good reference for this.
I was able to achieve your desired result using a single label.
Using a little math you can offset the baseline of the smaller text to achieve your desired result.
Objective-C
- (NSMutableAttributedString *)styleSalePriceLabel:(NSString *)salePrice withFont:(UIFont *)font
{
if ([salePrice rangeOfString:@"."].location == NSNotFound) {
return [[NSMutableAttributedString alloc] initWithString:salePrice];
} else {
NSRange range = [salePrice rangeOfString:@"."];
range.length = (salePrice.length - range.location);
NSMutableAttributedString *stylizedPriceLabel = [[NSMutableAttributedString alloc] initWithString:salePrice];
UIFont *smallFont = [UIFont fontWithName:font.fontName size:(font.pointSize / 2)];
NSNumber *offsetAmount = @(font.capHeight - smallFont.capHeight);
[stylizedPriceLabel addAttribute:NSFontAttributeName value:smallFont range:range];
[stylizedPriceLabel addAttribute:NSBaselineOffsetAttributeName value:offsetAmount range:range];
return stylizedPriceLabel;
}
}
Swift
extension Range where Bound == String.Index {
func asNSRange() -> NSRange {
let location = self.lowerBound.encodedOffset
let length = self.lowerBound.encodedOffset - self.upperBound.encodedOffset
return NSRange(location: location, length: length)
}
}
extension String {
func asStylizedPrice(using font: UIFont) -> NSMutableAttributedString {
let stylizedPrice = NSMutableAttributedString(string: self, attributes: [.font: font])
guard var changeRange = self.range(of: ".")?.asNSRange() else {
return stylizedPrice
}
changeRange.length = self.count - changeRange.location
// forgive the force unwrapping
let changeFont = UIFont(name: font.fontName, size: (font.pointSize / 2))!
let offset = font.capHeight - changeFont.capHeight
stylizedPrice.addAttribute(.font, value: changeFont, range: changeRange)
stylizedPrice.addAttribute(.baselineOffset, value: offset, range: changeRange)
return stylizedPrice
}
}
This yields the following: