How to apply bold and italics to an NSMutableAttributedString range?
Asked Answered
W

6

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?

Wriest answered 28/12, 2015 at 19:45 Comment(0)
I
14

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

Irving answered 28/12, 2015 at 23:19 Comment(5)
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.Wriest
@Owatch Here's a question that asks about enumerating. You'll still need to construct the fonts on the fly. There's no way around that.Irving
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.Wriest
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.Wriest
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.Irving
P
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()) ])
}
Penicillate answered 22/6, 2017 at 11:42 Comment(1)
Great answer, @PenicillateHannah
I
14

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

Irving answered 28/12, 2015 at 23:19 Comment(5)
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.Wriest
@Owatch Here's a question that asks about enumerating. You'll still need to construct the fonts on the fly. There's no way around that.Irving
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.Wriest
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.Wriest
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.Irving
W
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.

Wriest answered 29/12, 2015 at 12:0 Comment(0)
G
6

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

Getraer answered 27/5, 2019 at 16:10 Comment(1)
This works for characters such as Chinese or Korean which do not support Italic! ThanksHolmes
T
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

Tendance answered 19/2, 2020 at 19:50 Comment(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.Ferroelectric
M
1

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)

Mismate answered 14/5, 2022 at 9:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.