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.
Sources and verification
- The Swift Programming Language: Strings and CharactersSwift.org · Official documentation · Checked October 23, 2026Supports: Source for the implementation contract and explanation. The output was checked separately using the experiment included here.

