Swift & Objective-C

Swift String Length: Measure Hangul, Emoji, and Bytes

Compare Character, Unicode scalar, UTF-16, and UTF-8 counts; verify equivalent Hangul keys and slice a family emoji without splitting it.

2 min read
Cover image for Swift String Length: Measure Hangul, Emoji, and Bytes

Choose the counting unit before implementing a text limit. Swift count measures Character values, or extended grapheme clusters, while transfer limits usually measure bytes. The same displayed character can have different internal sizes.

Mechanism and design choice

The Hangul syllable in this experiment uses either one precomposed scalar or three combining Jamo scalars. Swift String treats them as canonically equivalent, including dictionary lookup. A family emoji shows several scalars forming one Character. Another language counting a different unit is not inherently wrong. Primary source

Reproduce the experiment

Unicode escapes fix the exact inputs. Compare the two Hangul forms’ Character, scalar, and UTF-8 counts. Measure the family emoji’s UTF-16 and UTF-8 lengths, insert it between A and B, and use prefix and String.Index. Check empty-string access separately.

This standalone example was run on 2026-10-23 with Apple Swift 6.4 targeting arm64 macOS, in Swift 6 language mode. It needs no network or additional packages.

Download the complete Swift file

Run the command in the download directory. Preconditions stop execution if a check fails. Do not use -Ounchecked: it removes these checks.

swift -swift-version 6 swift-string-grapheme-cluster-indexing.swift
let composed = "\u{D55C}"
let decomposed = "\u{1112}\u{1161}\u{11AB}"
precondition(composed == decomposed)
precondition(composed.count == 1 && decomposed.count == 1)
precondition(composed.utf8.count == 3 && decomposed.utf8.count == 9)
precondition(decomposed.unicodeScalars.count == 3)
let lookup = [composed: 42]
precondition(lookup[decomposed] == 42)

let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}"
precondition(family.count == 1 && family.unicodeScalars.count == 7)
precondition(family.utf16.count == 11 && family.utf8.count == 25)
let text = "A" + family + "B"
precondition(text.count == 3 && String(text.prefix(2)) == "A" + family)
let second = text.index(after: text.startIndex)
precondition(String(text[second]) == family)
let empty = ""
precondition(empty.first == nil && empty.prefix(2).isEmpty)
print("hangul=characters:1,scalars:1/3,utf8:3/9 equal:true dictionary:42")
print("family=characters:1,scalars:7,utf16:11,utf8:25 prefix=2 empty=safe")

Recorded output and interpretation

hangul=characters:1,scalars:1/3,utf8:3/9 equal:true dictionary:42
family=characters:1,scalars:7,utf16:11,utf8:25 prefix=2 empty=safe

Both Hangul forms have one Character, but use 3 versus 9 UTF-8 bytes; the decomposed form has three scalars. Equivalent dictionary lookup returns 42. The family emoji has one Character, seven scalars, eleven UTF-16 units, and twenty-five UTF-8 bytes. prefix(2) preserves the emoji.

Scope and limitations

Character is not a perfect model of every human notion of a letter; segmentation rules and Unicode versions matter. Do not assume three bytes per displayed Hangul character or emoji. Swift Dictionary does not always require explicit NFC normalization, but external comparison and encoding contracts need checking.

What to check next

Agree whether server and client limits count characters or UTF-8 bytes. Use Character-based APIs such as prefix for previews. For repeated random character access, weigh array conversion cost and memory. Continue with Optional to separate absent input from empty text.

Related experiment

Sources and verification