Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSAttributedString, change the font overall BUT keep all other attributes?

Say I have an NSMutableAttributedString .

The string has a varied mix of formatting throughout:

Here is an example:

This string is hell to change in iOS, it really sucks.

However, the font per se is not the font you want.

I want to:

for each and every character, change that character to a specific font (say, Avenir)

BUT,

for each and every character, keep the mix of other attributions (bold, italic, colors, etc etc) which was previously in place on that character.

How the hell do you do this?


Note:

if you trivially add an attribute "Avenir" over the whole range: it simply deletes all the other attribute ranges, you lose all formatting. Unfortunately, attributes are not, in fact "additive".

like image 830
Fattie Avatar asked May 01 '17 17:05

Fattie


2 Answers

Since rmaddy's answer did not work for me (f.fontDescriptor.withFace(font.fontName) does not keep traits like bold), here is an updated Swift 4 version that also includes color updating:

extension NSMutableAttributedString {     func setFontFace(font: UIFont, color: UIColor? = nil) {         beginEditing()         self.enumerateAttribute(             .font,              in: NSRange(location: 0, length: self.length)         ) { (value, range, stop) in              if let f = value as? UIFont,                let newFontDescriptor = f.fontDescriptor                 .withFamily(font.familyName)                 .withSymbolicTraits(f.fontDescriptor.symbolicTraits) {                  let newFont = UIFont(                     descriptor: newFontDescriptor,                      size: font.pointSize                 )                 removeAttribute(.font, range: range)                 addAttribute(.font, value: newFont, range: range)                 if let color = color {                     removeAttribute(                         .foregroundColor,                          range: range                     )                     addAttribute(                         .foregroundColor,                          value: color,                          range: range                     )                 }             }         }         endEditing()     } } 

Notes

The problem with f.fontDescriptor.withFace(font.fontName) is that it removes symbolic traits like italic, bold or compressed, since it will for some reason override those with default traits of that font face. Why this is so totally eludes me, it might even be an oversight on Apple's part; or it's "not a bug, but a feature", because we get the new font's traits for free.

So what we have to do is create a font descriptor that has the symbolic traits from the original font's font descriptor: .withSymbolicTraits(f.fontDescriptor.symbolicTraits). Props to rmaddy for the initial code on which I iterated.

I've already shipped this in a production app where we parse a HTML string via NSAttributedString.DocumentType.html and then change the font and color via the extension above. No problems so far.

like image 177
manmal Avatar answered Sep 29 '22 12:09

manmal


Here is a much simpler implementation that keeps all attributes in place, including all font attributes except it allows you to change the font face.

Note that this only makes use of the font face (name) of the passed in font. The size is kept from the existing font. If you want to also change all of the existing font sizes to the new size, change f.pointSize to font.pointSize.

extension NSMutableAttributedString {     func replaceFont(with font: UIFont) {         beginEditing()         self.enumerateAttribute(.font, in: NSRange(location: 0, length: self.length)) { (value, range, stop) in             if let f = value as? UIFont {                 let ufd = f.fontDescriptor.withFamily(font.familyName).withSymbolicTraits(f.fontDescriptor.symbolicTraits)!                 let newFont = UIFont(descriptor: ufd, size: f.pointSize)                 removeAttribute(.font, range: range)                 addAttribute(.font, value: newFont, range: range)             }         }         endEditing()     } } 

And to use it:

let someMutableAttributedString = ... // some attributed string with some font face you want to change someMutableAttributedString.replaceFont(with: UIFont.systemFont(ofSize: 12)) 
like image 33
rmaddy Avatar answered Sep 29 '22 12:09

rmaddy