Nate Stedman

Attributed

November 3rd, 2015

Rich-text formatting on iOS and OS X is performed with the NSAttributedString class. There are two components: a string value, and a set of substring ranges, each with an associated dictionary of attributes, which are a string key paired with an arbitrary value - since this is a Foundation class, these values must be of object types. These ranges cannot overlap. In a safe manner, attributed strings could be expressed as:

typealias AttributedString = [(Character, [String:AnyObject])]

API users do not need to interact with the backing representation at a low level, however - instead, attributes can be set across a range, or set for an entire string at initialization time. For example, given a mutable attributed string with a blue background color set across the entire string, adding a blue foreground color attribute to a middle range will automatically create three separate ranges: “blue background”, “blue background & red foreground”, and “blue background” again, each associated with the proper substring.


Attributed strings follow the standard Foundation pattern of immutability by default, with mutability available on request. Immutable strings can only be initialized with a single set of attributes:

NSAttributedString(string: "Red", attributes: [
    NSForegroundColorAttributeName: UIColor.redColor()
])

To create an attributed string with attributes that do not span the entire range, it is necessary to use the mutable subclass. There are two main methods, the first of which is creating the full string, then applying attributes:

let string = NSMutableAttributedString(string: "RedBlue")

string.addAttribute(
    NSForegroundColorAttributeName,
    value: UIColor.redColor(),
    range: NSMakeRange(0, 3)
)

string.addAttribute(
    NSForegroundColorAttributeName,
    value: UIColor.blueColor(),
    range: NSMakeRange(3, 4)
)

Since the attribute ranges are declared separately from the text, they will be need to be updated whenever the text changes. For example, if we decide to put a space between the two words, but forget to update the ranges, “RedBlue” will become “Red Blue”, with an unstyled “e”.

An alternative is to start with an empty mutable string, and append entire attributed strings – this is slightly longer than it needs to be, as it can be condensed by initializing the mutable string with the first item to be appended:

let string = NSMutableAttributedString()

string.appendAttributedString(NSAttributedString(
    string: "Red",
    attributes: [NSForegroundColorAttributeName: UIColor.redColor()]
))

string.appendAttributedString(NSAttributedString(
    string: "Blue",
    attributes: [NSForegroundColorAttributeName: UIColor.blueColor()]
))

This is verbose, but allows us to edit the text freely without worrying about breaking anything. However, if we want to make the string have a green background, it will be more complicated:

let string = NSMutableAttributedString()

string.appendAttributedString(NSAttributedString(
    string: "Red",
    attributes: [
        NSForegroundColorAttributeName: UIColor.blueColor(),
        NSBackgroundColorAttributeName: UIColor.greenColor()
    ]
))

string.appendAttributedString(NSAttributedString(
    string: "Blue",
    attributes: [
        NSForegroundColorAttributeName: UIColor.blueColor(),
        NSBackgroundColorAttributeName: UIColor.greenColor()
    ]
))

Although the green highlight applies across the entire string, it has to be declared within each substring. We could use the first method to add the green background afterwards, but this brings us back to the range problem. It’s a lot of boilerplate to obtain “RedBlue”.


A common solution to these issues is to use a markup parser, based on Markdown, HTML, or another format. There are many of these projects. However, string-based markup lacks safety — the string could have a parse error, specify an invalid color, or specify an unavailable font.


I’ve written a framework called Attributed, which provides a markup-like format in pure Swift code. Strings are expressed as a nested structure. For example, “RedBlue” is:

UIColor.greenColor().backgroundAttribute([
    UIColor.redColor().foregroundAttribute("Red"),
    UIColor.blueColor().foregroundAttribute("Blue")
].join())

Function calls on type extensions are the equivalent of “tags”. The functions accept an array of attributed string-like values – String, NSAttributedString, and the internal type returned by attribute functions.

Of course, foreground and background colors are not the only attributes supported – various types are extended to cover many standard attributes. The README provides a list of attributes, and an explanation of how to include custom attributes – for example, a social networking app might include a reference to a user as an attribute in post text that contains mentions.

For attributes that accept numerical values, but really ought to be enumerations, Attributed provides enumeration types and extends those instead of extending numerical types. For example, numerical values from 0 to 1 or 2 (depending on the platform) are used for enabling and disabling ligatures. Instead, Attributed provides a Ligature type with appropriate case values.


Attributed is available on Github. I recommend using Carthage to integrate it with a project, by adding this line to your Cartfile:

github "natestedman/Attributed"