Swift Unicode Strings and String.Index

Work with user-perceived characters safely, distinguish scalar and encoding views, and convert ranges across text APIs explicitly.

Lesson 33 of 43Intermediate9 min read

A visible character can be represented by more than one Unicode scalar and more than one encoded byte. Swift String treats Character as an extended grapheme cluster, which often matches what a person perceives as one character. This is why a numeric byte offset is not an appropriate universal index into human text.

The strings lesson introduces Unicode and basic indexing. This reference develops safe slicing, view differences and range interoperability. Choose the unit required by the task: a display preview, a UTF-8 file limit and an Objective-C UTF-16 range do not measure the same thing.

A string index belongs to a text traversal

String.Index represents a valid position in a string view rather than an arbitrary integer offset. StartIndex identifies the beginning, and endIndex marks the position after the last character. Use index(after:), index(before:) and supported offset operations to move through text.

EndIndex cannot be subscripted as an element. Empty text still has start and end boundaries but no Character to read. A limited offset operation can help avoid advancing beyond the boundary; if it fails, handle that result rather than force an invalid index.

Do not keep indices from one unrelated string and use them on another. Mutating text can invalidate previously captured positions according to the operation and view. Compute needed ranges from the current text, perform the operation, and reestablish positions afterward. Persist a semantic selection or offset policy rather than assume raw indices remain meaningful forever.

Repeatedly advancing from startIndex to increasing offsets can revisit the same prefix many times. For a full traversal, move one index forward or use a for loop over Characters instead. Likewise, do not assume count is a constant-time stored byte length. Choose algorithms that traverse once when processing large text, and only build an indexed Character array when its additional memory serves a real requirement.

Choose the right view for a real requirement

Character traversal is suitable for many user-facing operations such as taking a short visible prefix. UnicodeScalars exposes scalar values for Unicode-specific processing. UTF8 and UTF16 expose encoding code units for protocols and interoperability. Counting one view does not tell you the count of another.

A server may limit a field by encoded byte length. A local form showing a character counter should state whether it follows that server limit or user-perceived characters. A string with a small Character count can still have a larger byte count. Validate the actual protocol limit instead of inferring it from the visual length.

Swift string equality accounts for canonical equivalence, so different equivalent Unicode representations can compare equal as text. Raw encoded bytes can still differ. If a protocol requires a specific normalization or byte representation, make that policy explicit and apply the appropriate API rather than assume visual equality guarantees identical storage.

Range bridges need the original string

Foundation APIs inherited from NSString often use NSRange in UTF-16 units. Swift ranges over String use String.Index. Convert between them with the string-aware Range(nsRange, in: text) and NSRange(swiftRange, in: text) APIs rather than treating the range's integers as Character positions.

Conversion can fail when a supplied range is invalid for the string. Handle the optional result. Use the same current string that the external range refers to, and avoid applying a saved range after unrelated edits. If your interface accepts ranges from an external source, validate bounds before modifying text.

Substring is a view useful for short-lived parsing and slicing, but retaining one can retain the original string's backing storage. Convert to String when an extracted piece becomes a long-lived independent value. This is an ownership choice, not a requirement to copy every temporary slice during a local loop.

Official references

Read the Swift strings and characters guide and its official source. Test empty text, combining marks, emoji and boundary offsets. A robust text function states its unit, conversion policy, storage lifetime and invalid-range behavior instead of relying only on an English ASCII example.

Swift Unicode Strings and String.Index Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Count characters, scalars and bytes separately

The first visible character combines e and a combining acute accent. It uses two scalars and three UTF-8 bytes. The emoji is one scalar but uses four UTF-8 bytes and two UTF-16 code units. The final displayed accented character may look identical to a precomposed character even though its scalar representation differs.

Swift
let text = "e\u{301}🙂"
print(text.count)
print(text.unicodeScalars.count)
print(text.utf8.count)
print(text.utf16.count)

let second = text.index(after: text.startIndex)
print(text[second])
print(String(text[..<second]))
Output
2
3
7
4
🙂
é

Swift Unicode Strings and String.Index in Real Projects

Worked exercise: Build a visible-text preview

Return at most a chosen number of Characters, adding an ellipsis only when the input contains more. Use prefix rather than integer subscripting and convert the selected slice into an independent String. Zero or a negative limit produces an empty preview under this explicit policy.

Swift
func preview(_ text: String, limit: Int) -> String {
    guard limit > 0 else { return "" }
    let selected = text.prefix(limit)
    let result = String(selected)
    return selected.endIndex == text.endIndex ? result : result + "…"
}

print(preview("Cafe\u{301}🙂", limit: 4))
print(preview("Hi🙂", limit: 3))
print(preview("", limit: 3))
print(preview("Book", limit: 0))
Output
Café…
Hi🙂

The final two calls print empty lines. The accented final letter of the first preview stays intact because prefix counts Characters. This policy limits the source characters and then adds an ellipsis, so the resulting preview can contain one extra visible character. If the total output itself must fit the limit, reserve space for the marker explicitly.

Swift Unicode Strings and String.Index FAQ

Why does Swift not allow text[2] for a Character?

Unicode text does not map each visible Character to a fixed number of bytes or code units. Finding the third Character can require traversing boundaries, so an arbitrary Int does not represent a valid universal String position. Use String.Index operations or collection algorithms such as prefix. If an API needs byte offsets, operate on the encoding view deliberately and validate conversion before treating those offsets as human-text positions.

Are Unicode scalars the same as Characters?

No. A Character is an extended grapheme cluster that can contain several scalars, such as a base letter with combining marks or a multi-scalar emoji sequence. UnicodeScalars exposes the scalar-level representation, which is useful for different tasks. Choose the unit from the requirement and test composed text. Counting scalars as a user-facing character limit can split or miscount a symbol that a reader sees as one unit.

Can I use NSRange offsets directly as String indices?

No. NSRange from many Foundation text APIs uses UTF-16 code-unit offsets, while Swift Character traversal uses String.Index. Convert with the string-aware range APIs and handle invalid results. The same original text must be used during conversion, because edits can change the meaning of offsets. Emoji and combining characters make incorrect direct conversion visible, but even an ASCII-only test does not establish that the shortcut is safe for real input.

Does prefix preserve every kind of user interface meaning?

It preserves Character boundaries for this task, but a useful preview can require more policy: word boundaries, bidirectional text, markup removal, line limits or accessibility context. Prefix is a safe building block rather than a complete text-layout engine. Define the product requirement and test representative languages. If truncating a label changes its meaning, consider exposing the full value through another supported view or accessibility description.

Swift Unicode Strings and String.Index Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

Which operation safely selects a visible prefix without assuming fixed-width characters?

Swift Unicode Strings and String.Index Quiz

2 questions to check what you learned.

  1. What does String.count measure in this guide's text model?

    • Always exactly the number of Unicode scalars.
    • UTF-8 bytes.
    • Characters, represented as extended grapheme clusters.
  2. Why can NSRange require conversion before Swift string slicing?

    • Foundation ranges can use UTF-16 offsets rather than Character-based String.Index positions.
    • NSRange automatically encrypts text.
    • Swift strings never contain emoji.