67

I use NSAttributedString to generate a string with two different sizes. By default, its bottom alignment looks like this:

baseline aligned sizes

But I want to center it vertically, like this: vertically centered sizes

To be clear, this is a single attributed string, not two or more. This is a simplified example to describe my question, what I'd actually like to do is more complex.

Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
user2608857
  • 1,061
  • 2
  • 9
  • 11
  • have you considered using two separate views for that? So, for example have two `UILabel`s and after sizing them, align their frames centerY. It seems much cleaner to me than messing around with font sizes and baseline offsets. – CodingMeSwiftly Dec 16 '17 at 18:58

4 Answers4

101

I'd say the easiest thing to do is just manipulate the NSBaselineOffsetAttributeName attribute for the text in question:

NSBaselineOffsetAttributeName

The value of this attribute is an NSNumber object containing a floating point value indicating the character’s offset from the baseline, in points. The default value is 0.

To center, you'd take the difference between height of the large text and the height of the smaller text and halve it, then use that as the baseline adjustment.

Community
  • 1
  • 1
Ben Lachman
  • 3,083
  • 1
  • 28
  • 44
25

Here is a working example to vertically align smaller text using NSBaselineOffsetAttributeName.

NSString *bigString   = @"BIG";
NSString *smallString = @"Small String";
NSString *fullString = [NSString stringWithFormat:@"%@ %@", bigString, smallString];

NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:fullString];

NSRange bigStringRange = NSMakeRange(0, bigString.length);
NSRange smallStringRange = NSMakeRange(bigStringRange.length, smallString.length);

[string beginEditing];


//Set big string font and size
[string addAttribute:NSFontAttributeName
               value:[UIFont systemFontOfSize:28.0]
               range:bigStringRange];

//set small string font and size
[string addAttribute:NSFontAttributeName
               value:[UIFont systemFontOfSize:18.0]
               range:smallStringRange];

//Set small string baseline offset
[string addAttribute:NSBaselineOffsetAttributeName
               value:[NSNumber numberWithFloat:3.0]  //adjust this number till text appears to be centered
               range:smallStringRange];

[string endEditing];
Yas Tabasam
  • 10,517
  • 9
  • 48
  • 53
  • 7
    Sorry, but this is more like a trial and error hack, not a real solution. But this can be a solution if combined with some math on-the-fly. – Duck Jan 05 '15 at 20:28
  • Well, it works as it is. You just need to figure out baseline value which should not take more than a couple of tries. – Yas Tabasam Feb 04 '15 at 12:10
  • Of course, even if you grab all the font metrics and do the right thing, you potentially have to recompute the baseline offset for every range in the string as soon as you add a single character in a different size.... – dgatwood Sep 28 '16 at 23:28
  • 6
    Never trust a programmer who won’t use a hack once in a while… – mxcl Jan 26 '17 at 03:08
11

YasT's answer in Swift:

Swift 4

let bigString = "BIG"
let smallString = "Small String"
let fullString = "\(bigString) \(smallString)"
let string = NSMutableAttributedString(string: fullString)

let bigStringRange = NSRange(location: 0, length: bigString.utf16.count)
let smallStringRange = NSRange(location: bigStringRange.length + 1, length: smallString.utf16.count)

let bigStringFontSize: CGFloat = 28
let smallStringFontSize: CGFloat = 18

string.beginEditing()

string.addAttribute(.font, value: UIFont.systemFont(ofSize: bigStringFontSize), range: bigStringRange)
string.addAttribute(.font, value: UIFont.systemFont(ofSize: smallStringFontSize), range: smallStringRange)
string.addAttribute(.baselineOffset, value: (bigStringFontSize - smallStringFontSize) / 2, range: smallStringRange)

string.endEditing()

Swift 3

let bigString = "BIG"
let smallString = "Small String"
let fullString = "\(bigString) \(smallString)"
let string = NSMutableAttributedString(string: fullString)

let bigStringRange = NSRange(location: 0, length: bigString.utf16.count)
let smallStringRange = NSRange(location: bigStringRange.length + 1, length: smallString.utf16.count)

let bigStringFontSize: CGFloat = 28
let smallStringFontSize: CGFloat = 18

string.beginEditing()

string.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: bigStringFontSize), range: bigStringRange)
string.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: smallStringFontSize), range: smallStringRange)
string.addAttribute(NSBaselineOffsetAttributeName, value: (bigStringFontSize - smallStringFontSize) / 2, range: smallStringRange)

string.endEditing()
Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
3

Better solution is calculating NSBaselineOffsetAttributeName from fonts typography (short article https://www.raizlabs.com/dev/2015/08/advanced-ios-typography/)

Set attribute for second part of attributed string.

secondPartAttributes[NSBaselineOffsetAttributeName] = @((firstFont.xHeight - secondFont.xHeight)/2);
Raman
  • 211
  • 2
  • 3
  • Very nice [article](https://www.raizlabs.com/dev/2015/08/advanced-ios-typography/) you referenced there! Even more adorable would be a `NSAttributedString` extension which extracts the biggest `fontSize` from the attributes of a given object and adjusts the baseline of all the other ranges with smaller fonts like your example indicates and returns that `NSMutableAttributedString`. I hope to have time for something like that soon. – FBente Apr 27 '18 at 15:27
  • You should use capHeight, not xHeight. – Ilias Karim May 07 '21 at 19:10