One of the most confusing points for developers moving to Swift from another language is strings. text[0] doesn’t work, text[2..<5] doesn’t either, and you must create a separate index type and write text.index(text.startIndex, offsetBy: 2). In Python, this would take one character.
This isn’t because the Swift team couldn’t design the API. Quite the opposite: it honestly exposes that “the nth character of a string” isn’t a simple concept. Other languages hide this complexity and sometimes return the wrong answer. Swift chose to always return the right answer, even when that is less convenient. In this sixth and final Swift Basics installment, we dig into why strings are genuinely difficult.
The flow that can fail while converting a string to a number is covered with its error model in How to choose among Swift throws, try, and Result.
Why “How many characters?” is difficult — the world of Unicode
Start with this question: how many characters are in “café”? Four, obviously—but computers have two possible answers. é can be stored as one composed code point (U+00E9), or as e (U+0065) plus a combining accent (U+0301). They look identical, but the internal code-point counts are 1 and 2.
Korean makes this issue even more tangible. A Korean syllable can be one precomposed syllable code point (U+AC01), or a combination of three jamo letters. If you have worked with macOS filenames, you may have seen Korean filenames stored as NFD (Normalization Form D—a Unicode normalization form that decomposes letters into jamo) appear split into jamo on another system. The same “character” can contain one or three code points.
Emoji add another layer. The family emoji 👨👩👧👦 joins four person emoji with zero-width joiners (ZWJ, invisible characters that group emoji as one character), totaling seven code points. In UTF-16 code units, it takes 11. To the eye, however, it is one character.
That is why Unicode defines a separate unit for “one character as perceived by a person”: the extended grapheme cluster. é, a composed Korean syllable, and the family emoji are each one grapheme cluster.
Language design choices — is a wrong answer faster, or a correct answer slower?
We can now compare how each language handles the “nth character.”
JavaScript’s "👨👩👧👦".length is 11 because it counts UTF-16 code units. Java behaves the same way. Python 3’s len returns 7 because it counts code points. All conflict with the human intuition of “one character.” Naively slicing strings containing emoji can split an emoji in the middle and produce broken text.
Swift’s "👨👩👧👦".count is 1 because Swift’s Character represents a grapheme cluster, not a code point. count is identical regardless of how é is stored, and == returns true with canonical normalization considered. It always answers according to what people see.
That accuracy has a cost. Grapheme clusters have variable length, so finding the nth character requires scanning boundaries from the start. That is why Swift strings have no integer indices. An API making text[7] appear O(1) would hide an actual O(n) cost. Using text[i] inside a loop makes it O(n²), and Swift chooses not to encourage that. The opaque String.Index type makes it explicit that a position was found by counting. The principle from Part 1 of the philosophy series—don’t hide costs—applies to strings too.
Practical toolkit — working with strings without indices
Now for practice. Even without integer indices, safer tools handle most tasks.
For trimming both ends, use prefix and suffix. text.prefix(10) safely return the first 10 characters (graphemes), or as many as available without crashing. No substring-index calculation is needed for preview text.
For searching, use range(of:) and firstIndex(of:). When you need a position, it is usually the position of specific content. Put the returned Range or Index directly into the subscript; no integer conversion is needed.
For splitting, use split. text.split(separator: ",") replaces manual index traversal.
If you truly need the nth character, use Array. For algorithms requiring random access by character, convert to Array(text). Pay O(n) once for conversion, then every access is O(1). This is far better than calling index(offsetBy:) on every loop.
When bytes matter, choose a view explicitly. Sometimes you need byte count rather than character count, such as network payloads or DB column limits. Swift makes the unit explicit with text.utf8.count, text.utf16.count, and text.unicodeScalars.count. Note that NSString.length, encountered in UserDefaults or older APIs, counts UTF-16 and can differ from Swift’s count.
One trap: indices from different strings are incompatible. Using a String.Index obtained from a on b can crash or produce nonsense. An index belongs to that string at that point in time. After modifying a string, treat previous indices as invalid.
Why this matters especially for Korean developers
When building Korean services, this knowledge becomes practical rather than theoretical.
Nickname length limits are a common example. If the server and Swift client count “up to 10 characters” in different units, a nickname accepted by the client may be rejected by the server. If the server limits UTF-8 bytes, Korean uses three bytes per character, so 10 characters means 30 bytes. First agree as a team on what to count; in Swift, deliberately choose count for graphemes or utf8.count for bytes.
Jamo decomposition is another issue. Because of NFD, Korean strings from external systems can look identical while containing different code points. Swift’s == considers normalization, so it usually works, but when using external strings as hash-based dictionary keys or passing them to another language, explicitly normalize to NFC (Normalization Form C—a form that stores jamo combined into precomposed letters) with precomposedStringWithCanonicalMapping.
For features such as initial-consonant search in search and autocomplete, you instead need to decompose graphemes into jamo; the unicodeScalars view is the starting point. Once you understand views, this becomes “iterate another view,” not “find a special library.”
Results confirmed by running the code
In Apple Swift 6.3.3, we compared a precomposed form 한 with a composed form made from three jamo 한. The composed string has three Unicode scalars, but String.count counts it as one character, and the two forms were equal under canonical-equivalence comparison.
string=characters:1,scalars:3,canonically-equal:true
After confirming this, I expose units in variable names when implementing length limits. For characterLimit, use count; for transfer size, use utf8.count, and document the same unit in the server contract. Agreeing on one unitless length can easily make Korean and emoji diverge between client and server.
Summary
- “Nth character” is difficult because one Unicode character (a grapheme cluster) consists of a variable number of code points. é, composed Korean syllables, and ZWJ emoji are representative cases.
- Other languages’ length may count code units or code points and conflict with intuition. Swift’s count counts graphemes and always answers from the human perspective.
- The tradeoff is O(n) random access, so Swift removed integer indices to avoid hiding the cost.
- In practice, prefix/suffix, range(of:), and split solve most problems; use an Array for random access and explicitly use the utf8 view for byte counts.
- For Korean services, agree on the unit for length validation and normalize with NFC/NFD where appropriate.
That concludes the six-part Basics series. Next comes the intermediate series. The first topic is Swift memory management’s core: ARC (Automatic Reference Counting) and choosing between weak and unowned. It is the main installment of the retain-cycle story previewed in the closures article.
Recommended reading
- [Swift Basics #5] A complete map of Swift error handling: when to use throws, try?, try!, or Result
- [Swift Basics #4] Using Swift guard properly: early exits that flatten the pyramid of doom
- The iOS Coordinator pattern: removing screen-transition code from view controllers
Sources and verification
- Swift StringApple Developer Documentation · Official documentation · Checked August 26, 2026Supports: String's Character collection model, Unicode canonical equivalence, string views, and index APIs
- The Swift Programming Language: Strings and CharactersSwift.org · Official documentation · Checked August 26, 2026Supports: Extended grapheme clusters, composed Hangul, and the cost of count and integer indexing

![Cover image for [Swift Basics #6] Why can't strings use text[0]?](/assets/images/posts/18529e8e-628f-4b41-8e48-680d2ff6f480/1.jpg)