here is a version that can be used with both macOS and iOS (should have done this earlier)
//
// styledText.swift
// StyledText
//
// Created by David Sisemore on 12/3/23.
//
#if os(iOS)
import UIKit
typealias NSFont = UIFont
typealias NSColor = UIColor
typealias NSFontDescriptor = UIFontDescriptor
let traitBOLD = NSFontDescriptor.SymbolicTraits.traitBold
let traitITALIC = NSFontDescriptor.SymbolicTraits.traitItalic
#else
import Cocoa
let traitBOLD = NSFontDescriptor.SymbolicTraits.bold
let traitITALIC = NSFontDescriptor.SymbolicTraits.italic
#endif
import Foundation
extension NSMutableAttributedString {
func bold(_ state : Bool, _ start:Int = -1, _ length:Int = -1) {
updateTraits(traitBOLD,state, location: start, length: length)
}
func italic(_ state : Bool, _ start:Int = -1, _ length:Int = -1) {
updateTraits(traitITALIC,state, location: start, length: length)
}
func underline(_ state : Bool, _ start:Int = -1, _ length:Int = -1) {
updateAttribute(.underlineStyle, value: state ? 1 : 0, location: start, length: length)
}
func strikethrough(_ state : Bool, _ start:Int = -1, _ length:Int = -1) {
updateAttribute(.strikethroughStyle, value: state ? 1 : 0, location: start, length: length)
}
func font(_ font:NSFont, _ start:Int = -1, _ length:Int = -1) {
updateAttribute(.font, value: font, location: start, length: length)
}
func textColor(_ color:NSColor, _ start:Int = -1, _ length:Int = -1) {
updateAttribute(.foregroundColor, value: color, location: start, length: length)
}
func backgroundColor(_ color:NSColor, _ start:Int = -1, _ length:Int = -1) {
updateAttribute(.backgroundColor, value: color, location: start, length: length)
}
/*********************************/
/*** Private Support Functions ***/
/*********************************/
private func getRange(location: Int, length: Int) -> NSRange? {
let count : Int = self.string.count
let start : Int = max(0,location-1) // zero-based!
var size : Int = min(count,length)
if start == 0 && size<0 { size = count } // if start/len = -1, the default to total string
if start<0 || start>count || size==0 { return nil }
//
let r = NSRange(location: start, length: size)
return r
}
private func updateAttribute(_ name: NSAttributedString.Key, value: Any, location: Int, length: Int) {
let r = getRange(location: location, length: length)
if r == nil { return }
self.addAttribute(name, value: value, range: r!)
}
private func updateTraits(_ new_trait : NSFontDescriptor.SymbolicTraits,_ state:Bool,location: Int, length: Int) {
let range = getRange(location: location, length: length)
if range == nil { return }
//
self.beginEditing()
self.enumerateAttribute(.font, in: range!, options: .longestEffectiveRangeNotRequired) { (value, r, stop) in
if let font = value as? NSFont { //Confirm the attribute value is actually a font
self.removeAttribute(.font, range: r)
var traits: NSFontDescriptor.SymbolicTraits = NSFontDescriptor.SymbolicTraits()
traits.insert(font.fontDescriptor.symbolicTraits)
if state==false {
traits.remove(new_trait)
} else {
traits.insert(new_trait)
}
#if os(iOS)
self.addAttribute(.font, value: NSFont(descriptor: font.fontDescriptor.withSymbolicTraits(traits)!, size: font.pointSize),range: r)
#else
self.addAttribute(.font, value: NSFont(descriptor: font.fontDescriptor.withSymbolicTraits(traits), size: font.pointSize)!,range: r)
#endif
}
}
self.endEditing()
}
}