Styled Text for Swift

This is an extension of NSMutableAttributedString which is akin to Xojo’s StyledText

each function is checked to insure it is within the range of the supplied string and sets any combination of the following attributes

  • Bold
  • Italic
  • Underline
  • Strikethrough
  • Font (name and size)
  • TextColor
  • BackgroundColor

right now the syntax is for macOS, but I will be converting it to support both macOS and iOS
as it is an extension, all the other functions of NSMutableAttributedString are still available

SEE UPDATED CODE BELOW

Example of Use

var temp = NSMutableAttributedString(string: "1234567890")
temp.font(NSFont(name:"MENLO",size:50)!,0,10)
temp.textColor(NSColor.orange)
temp.underline(true)
temp.textColor(NSColor.red,1,5)
temp.bold(true,2,3)
temp.italic(true,2,3)
temp.italic(false,3,1)
temp.backgroundColor(.yellow,3,5)
temp.strikethrough(false,3,5)

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

Thanks for sharing this, is there a reason as to why you used NSAttributedString and not the Swift Attributed String? Apart from it requires macOS 12.0 or newer.

To be honest… it is what I’ve used for years…

but on the other hand… “AttributedString” doesn’t seem to be consistent

SwiftUI lacks the support for the old NSAttributedString but fully supports this new type.
On the other hand, UIKit supported NSAttributedString but lacked the support of AttributedString in most APIs.

So it looks like as of the moment, use of UIKIT it is still safer to use NSAttributedString but for SwiftUI it isn’t

and for iOS it requires iOS15 and greater

not to mention… the code requires changes… as the current extenstion is not valid for AttributedString (over 15 compile errors)

Good enough answer for me :slight_smile:

Gotchya.

Gulp…

I have and am adding more features to the extension

So far today :slight_smile:

  • create Attributed String directly from HTML
  • create Attributed String directly from Markdown (specific Apple supported subset)
  • convert Attributed String directly TO HTML
  • convert Attributed String directly TO Markdown

Due to the way the attributes are stored, strings created from HTML or Markdown will have their formatting overridden, if you change FONT, BOLD or ITALIC using the specific supplied functions

For the time being, I will not be posting these updates, but will freely supply them to anyone who requests them, just drop me a private message with an email address

1 Like

ok,… these now seem to work

* let html = temp.toHTML()!
* let markdown = temp.toMARKDOWN()!
* temp.fromHTML("<br><font size=50><b>BOLD<b><i>italic</i><u>test</u")
* temp.fromMARKDOWN("1***2***~~<u>**3**</u>***<u>4</u>***567~~<u>890</u>.")

the markdown functions only support a subset…

  • Bold
  • Italic
  • Underline (boy THAT took some effort)
  • Strikethru