Create Lists with AttributedString in iOS 15

Stuart Moore
4 min readJun 21, 2021

Attributed strings are very powerful for laying out text; they’re more than just a way to convert HTML into bold ranges. When I worked at The Washington Post, I wrote an entire article layout engine utilizing a single NSAttributedString (with heavy use of text attachments).

iOS 15 brings attributed strings into the value-type world with the aptly named AttributedString. With this, comes Markdown parsing, but notably absent — as of beta 1 — are block-based rendering such as lists.

In this series, I’ll go over how I create lists in UIKit. Since NSParagraphStyle is part of UIKit’s scope, you won’t be able to use this on SwiftUI. There is no analogous paragraph styling available on SwiftUI that I’m aware of.

The Anatomy of a List

Before you can create a list, you need to know how exactly a list is stored in a string. There are always more than one way to accomplish anything in code, but this is my preferred method.

We’ll get to Markdown later, right now, let’s look at the desired end result string you’ll pass to your UILabel or UITextView via AttributedString:

It looks like a mess of characters; it breaks down to:

The first character, "•" (option + 8), is the the bullet character. Any character can be used here (numbers, shapes, or even images), but a consistent character for each item will make this list a little simpler.

Next is an escaped tab character: "\t". This is where the layout comes into play. The paragraph style will use these tabs to decide where to place the next characters rendered.

After that is the content of the list item, in this case, "Item 1".

That pattern repeats itself for for each item, all joined by "\u{2029}": the escape sequence for a “paragraph separator” Unicode character. Although the newline character, "\n", will work as well, using an explicit paragraph separator is good practice. It also balances out well with the “line separator” "\u{2028}", which can be used to create new lines inside a list item.

The Paragraph Style

If you display the string with the default attributes, you won’t get a very pretty list. Besides settings a font on the attributed string, we need to set a paragraph style to accurately space each item and bullet out:

These are the two main properties that affect list layout: tabStops and headIndent. Though, you’ll want to fill in the other properties to match the rest of your text layout. The two comments are values we’ll need to calculate from our string.

tabStops are an array of alignments and locations used to align text after each tab character. Since we use a tab after the bullet, the first characters of the list item will align to the tab stop we set (assuming that the location of the tab stop is after the end of the bullet character).

headIndent is the amount of space from the leading edge of the view to the beginning of every line of text after the first in a paragraph; that includes wrapped text and text after line separators ("\u{2028}").

Given that our tab is in the first line — and head indent affects all but the first line — these two values should be the same to align the leading edge of our text. But what should that value be? If we set it to exactly the same width as the bullet, the text will bump right up against it. If we add a desired padding, we can exactly position the text along any column.

To get the width of the bullet, we have to dive back into NSAttributedString:

Add the padding you want to that value, and set it on your paragraph style:

Now all we have to do is use the paragraph style:

And set that on your UILabel or UITextView.

In subsequent articles, we’ll look at how to align numbered lists (where the “bullet” width varies) and converting from Markdown.

--

--

Stuart Moore

iOS at Capital One, Formerly at Washington Post & iA. America’s Sweetheart™