Micrified
Micrified

Reputation: 3640

How to apply bold and italics to an NSMutableAttributedString range?

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?

Upvotes: 29

Views: 31955

Answers (6)

Robin Macharg
Robin Macharg

Reputation: 1848

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)

Upvotes: 1

nyg
nyg

Reputation: 2542

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()) ])
}

Upvotes: 41

Vasily  Bodnarchuk
Vasily Bodnarchuk

Reputation: 25294

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

Upvotes: 4

qsmy
qsmy

Reputation: 423

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

Upvotes: 6

Micrified
Micrified

Reputation: 3640

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.

Upvotes: 8

user4151918
user4151918

Reputation:

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

Upvotes: 15

Related Questions