29

I've been trying to apply combinations of NSFontAttributes to NSMutableAttributedString's lately and I simply can't find a thorough explanation on how to do it without removing other attributes.

I've searched a bunch, and found this question pertaining to how to do it with HTML, and then this question about how to find where text has been bolded or italicized, but nothing on how to actually do it.

Currently, I try to format stings as follows:

Italics: [mutableAttributedString addAttribute: NSFontAttributeName value:[fontAttributes valueForKey:CXItalicsFontAttributeName] range:r];

Bold: [mutableAttributedString addAttribute:NSFontAttributeName value:[fontAttributes valueForKey:CXBoldFontAttributeName] range:r];

Where the constants CXItalicsFontAttributeName and CXBoldAttributeName extract the following two values from a dictionary respectfully:

UIFont *italicsFont = [UIFont fontWithName:@"Avenir-BookOblique" size:14.0f];
UIFont *boldFont = [UIFont fontWithName:@"Avenir-Heavy" size:14.0f];

I know this mustn't be the right way to go about formatting, as the NSAttributedString standard attributes don't include a ItalicsFontAttribute or BoldFontAttribute, but I can't find the properly way to do this. Can anyone assist me?

Cem Schemel
  • 442
  • 3
  • 13
Micrified
  • 3,338
  • 4
  • 33
  • 59

6 Answers6

41

In Swift and using an extension:

extension UIFont {

    func withTraits(_ traits: UIFontDescriptor.SymbolicTraits) -> UIFont {

        // create a new font descriptor with the given traits
        guard let fd = fontDescriptor.withSymbolicTraits(traits) else {
            // the given traits couldn't be applied, return self
            return self
        }
            
        // return a new font with the created font descriptor
        return UIFont(descriptor: fd, size: pointSize)
    }

    func italics() -> UIFont {
        return withTraits(.traitItalic)
    }

    func bold() -> UIFont {
        return withTraits(.traitBold)
    }

    func boldItalics() -> UIFont {
        return withTraits([ .traitBold, .traitItalic ])
    }
}

Example:

if let font = UIFont(name: "Avenir", size: 30) {
    let s = NSAttributedString(string: "Hello World!", attributes: [ NSFontAttributeName: font.italic() ])
    let t = NSAttributedString(string: "Hello World!", attributes: [ NSFontAttributeName: font.boldItalic()) ])
}
fredpi
  • 8,414
  • 5
  • 41
  • 61
nyg
  • 2,380
  • 3
  • 25
  • 40
15

If you're applying each (bold or italic) trait individually, you need to make sure that bold and italic ranges don't overlap or one trait will overwrite the other.

The only way to apply both bold and italic traits to a range is to use a font which is both bold and italic, and apply the both traits at once.

let str = "Normal Bold Italics BoldItalics"

let font = UIFont(name: "Avenir", size: 14.0)!
let italicsFont = UIFont(name: "Avenir-BookOblique", size: 14.0)!
let boldFont = UIFont(name: "Avenir-Heavy", size: 14.0)!
let boldItalicsFont = UIFont(name: "Avenir-HeavyOblique", size: 14.0)!

let attributedString = NSMutableAttributedString(string: str, attributes: [NSFontAttributeName : font])
attributedString.addAttribute(NSFontAttributeName, value: boldFont, range: NSMakeRange(7, 4))
attributedString.addAttribute(NSFontAttributeName, value: italicsFont, range: NSMakeRange(12, 7))
attributedString.addAttribute(NSFontAttributeName, value: boldItalicsFont, range: NSMakeRange(20, 11))

enter image description here

  • 1
    This method seems impractical. It means that I will need to create as many combinations as possible to try and account for all cases. I was hoping to avoid it. – Micrified Dec 28 '15 at 23:21
  • @Owatch Here's [a question that asks about enumerating](http://stackoverflow.com/q/14161254/4151918). You'll still need to construct the fonts on the fly. There's no way around that. –  Dec 28 '15 at 23:47
  • I had selected your answer as the solution earlier as I couldn't find any alternatives, but have since found something a bit more suited to what I was looking for, so I removed the checkmark. I apologize for being inconsistent. – Micrified Dec 29 '15 at 12:01
  • My original question was written knowing that a BoldItalicsFont did exist. However, I misunderstood that these things could not be 'layered' so to speak, and that they were entirely individual fonts of their own I would have to apply. I can see that it was unclear though, so I guess if you think your answer is best suited to the question as it stands, I can select it again. – Micrified Dec 29 '15 at 12:08
  • It's all good! Unfortunately, there's just no way to apply traits independent of the font, since those traits are specific to each font. –  Dec 29 '15 at 12:12
8

The best solution to my problem so far has been to make use of the UIFontDescriptor class to provide me with the necessary UIFont for each case I encounter.

For instance, as I want to use Avenir-Book as my primary font with size 14, I can create a UIFontDescriptor as follows:

UIFontDescriptor *fontDescriptor = [UIFontDescriptor fontDescriptorWithName:@"Avenir-Book" size:14.0f];

Next, if I wish to obtain the Italicized, Bolded, or combination of both, I have simple to do as follows:

NSString *normalFont = [[fontDescriptor fontAttributes]valueForKey:UIFontDescriptorNameAttribute];
NSString *italicsFont = [[[fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic]fontAttributes]valueForKey:UIFontDescriptorNameAttribute];
NSString *boldFont = [[[fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold]fontAttributes]valueForKey:UIFontDescriptorNameAttribute];
NSString *boldAndItalicsFont = [[[fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold | UIFontDescriptorTraitItalic]fontAttributes]valueForKey:UIFontDescriptorNameAttribute];

And this indeed produces the required fonts when printed:

NSLog(@"Normal Font: %@ Size: %@\n",normalFont,[[fontDescriptor fontAttributes]valueForKey:UIFontDescriptorSizeAttribute]);
NSLog(@"Italics Font: %@\n",italicsFont);
NSLog(@"Bold Font: %@\n",boldFont);
NSLog(@"Bold and Italics Font: %@\n",boldAndItalicsFont);

Output:

Normal Font: Avenir-Book Size: 14
Italics Font: Avenir-BookOblique
Bold Font: Avenir-Black
Bold and Italics Font: Avenir-BlackOblique

The advantage here is that I no longer need to create the individual font types myself, and a font from the family is sufficient to do so.

Pablo
  • 2,834
  • 5
  • 25
  • 45
Micrified
  • 3,338
  • 4
  • 33
  • 59
5

Checkout NSAttributedString.Key.obliqueness. Setting the value of this key to values other than 0 make the text italics with different degree.

qsmy
  • 383
  • 3
  • 14
4

Details

  • Swift 5.1, Xcode 11.3.1

Solution

extension UIFont {
    class func systemFont(ofSize fontSize: CGFloat, symbolicTraits: UIFontDescriptor.SymbolicTraits) -> UIFont? {
        return UIFont.systemFont(ofSize: fontSize).including(symbolicTraits: symbolicTraits)
    }

    func including(symbolicTraits: UIFontDescriptor.SymbolicTraits) -> UIFont? {
        var _symbolicTraits = self.fontDescriptor.symbolicTraits
        _symbolicTraits.update(with: symbolicTraits)
        return withOnly(symbolicTraits: _symbolicTraits)
    }

    func excluding(symbolicTraits: UIFontDescriptor.SymbolicTraits) -> UIFont? {
        var _symbolicTraits = self.fontDescriptor.symbolicTraits
        _symbolicTraits.remove(symbolicTraits)
        return withOnly(symbolicTraits: _symbolicTraits)
    }

    func withOnly(symbolicTraits: UIFontDescriptor.SymbolicTraits) -> UIFont? {
        guard let fontDescriptor = fontDescriptor.withSymbolicTraits(symbolicTraits) else { return nil }
        return .init(descriptor: fontDescriptor, size: pointSize)
    }
}

Usage

font = UIFont.italicSystemFont(ofSize: 15).including(symbolicTraits: .traitBold)
font = UIFont.systemFont(ofSize: 15, symbolicTraits: [.traitBold, .traitItalic])
font = font.excluding(symbolicTraits: [.traitBold]
font = font.withOnly(symbolicTraits: [])

Full sample

Do not forget to paste the solution code here.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        addLabel(origin: .init(x: 20, y: 20), font: .systemFont(ofSize: 15, symbolicTraits: [.traitBold, .traitItalic]))
        addLabel(origin: .init(x: 20, y: 40), font: UIFont.italicSystemFont(ofSize: 15).including(symbolicTraits: .traitBold))
        guard let font = UIFont.systemFont(ofSize: 15, symbolicTraits: [.traitBold, .traitItalic]) else { return }
        addLabel(origin: .init(x: 20, y: 60), font: font.excluding(symbolicTraits: [.traitBold]))
        addLabel(origin: .init(x: 20, y: 80), font: font.withOnly(symbolicTraits: []))
    }

    private func addLabel(origin: CGPoint, font: UIFont?) {
        guard let font = font else { return }
        let label = UILabel(frame: .init(origin: origin, size: .init(width: 200, height: 40)))
        label.attributedText = NSAttributedString(string: "Hello World!", attributes: [.font: font, .foregroundColor: UIColor.black ])
        view.addSubview(label)
    }
}

Result

enter image description here

Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
  • 1
    Very helpful solution. After seeing this, my plan for the text apps I will build is to provide users with only fonts that have both bold and italic traits. – Cable W Oct 19 '21 at 07:16
0

Not a direct answer to the question, per-se, but SwiftUI, as of iOS 15, supports Markdown natively in Text views. This may suffice if your need is only emboldening or italicising a portion of a string (and you're using SwiftUI, of course!) e.g.

VStack {
    Text("This is regular text.")
    Text("* This is **bold** text, this is *italic* text, and this is ***bold, italic*** text.")
    Text("~~A strikethrough example~~")
    Text("`Monospaced works too`")
    Text("Visit Apple: [click here](https://apple.com)")
}

(The example is lifted straight from hackingwithswift.com)

Robin Macharg
  • 1,468
  • 14
  • 22