Why do ranges break when you move between String, AttributedString and NSAttributedString?

2.8.2 · L3 · Learning

Predict first

What does this print?

let t = "👍 hi"
let r = t.range(of: "hi")!
print(NSRange(r, in: t))
  • {2, 2}
  • {3, 2}
  • {4, 2}

Predict first

What does this print?

let t = "👍 hi"
print(t.count, t.utf16.count)
  • 4 4
  • 4 5
  • 5 5

Mental model. Two people counting the same sentence, one by letters and one by keystrokes. They agree until an emoji shows up.

Swift’s String and AttributedString use Range<String.Index> over characters. NSAttributedString, NSString and the text controls use NSRange over UTF-16 code units. An emoji is one Character but two UTF-16 units, so the offsets disagree as soon as the text contains one.

Convert with NSRange(range, in: string) and Range(nsRange, in: string), never by reusing a number. For a full-length range use utf16.count (or NSString.length), not count. When you bridge styled text, include the attribute scope: AttributedString(ns, including: \.uiKit), otherwise UIKit attributes such as font are dropped.

The emoji is two UTF-16 units and the space one, so "hi" starts at 3.

Four characters, but five UTF-16 units. A length computed from count is one short.

Common traps

  • NSRange(location: 0, length: s.count). Too short for any string containing an emoji or accent made of several scalars.
  • Dropping the scope when bridging. The converted string silently loses its fonts and colours.

They might follow up with

  • Which APIs use NSRange? UITextView, NSRegularExpression, NSAttributedString, and most older Foundation text APIs.
  • Is the Regex type affected? No, it works in Swift ranges.

Related questions

  • 2.8.1 What is AttributedString, and how is it different from NSAttributedString?
  • 1.9.2 Character vs Unicode.Scalar vs UTF-8 code unit?
  • 2.9.3 What does bridging cost, and where does it show up in a profile?

← What is AttributedString, and how is it different from…   When do you use Scanner, and how does it compare with Regex and split? →