Strings and characters
Why "héllo".count is 5, why you cannot write text[3], and why both of those are the right decisions.
By the end you will be able to
- Explain what a Swift Character is and why it is not one byte
- Manipulate strings with prefix, suffix, split, trimming and interpolation
- Say why String is not randomly indexable and what to use instead
The family emoji 👨👩👧 is built from three people joined by invisible connector characters, occupying 18 bytes. What is its count in Swift?
A String is a collection of Characters
let greeting = "Hello"
greeting.count // 5
greeting.isEmpty // false
greeting.uppercased() // "HELLO"
for character in greeting {
print(character)
}
Strings work with the collection methods you already know — map, filter, first, contains, sorted. The elements are Character values.
What a Character really is
A Character is a grapheme cluster: everything a reader would call one character. That may be one Unicode scalar, or several.
let e1: Character = "é" // one scalar: U+00E9
let e2: Character = "e\u{301}" // two scalars: e + combining accent
e1 == e2 // true — they look the same, so they are equal
Swift compares strings by canonical equivalence: two strings that a reader would see as identical compare equal, even when their bytes differ. The anchor: é is one letter you can type two ways — a dedicated é key, or e followed by a combining accent. Two keystroke sequences, one letter. Comparing bytes is counting keystrokes; comparing canonically is reading the word — and reading the word is almost always what you want.
The cost is that a Character is not a fixed size in memory. Which leads directly to the next point.
You cannot write text[3]
let text = "Hello"
// text[3] ← does not compile
Because characters vary in byte length, jumping to "the fourth character" requires walking from the start — the way finding the 500th word in a book means reading from page one, because words vary in width, while finding the 500th page is instant, because pages are uniform. Arrays are pages; strings are words. If text[3] were allowed it would look like an instant operation and secretly be a scan — so Swift makes you say what you mean with String.Index:
let index = text.index(text.startIndex, offsetBy: 3)
text[index] // "l"
text[text.startIndex] // "H"
text[..<text.index(text.startIndex, offsetBy: 2)] // "He"
In practice you rarely need this. The collection methods cover most real work:
text.prefix(3) // "Hel"
text.suffix(2) // "lo"
text.dropFirst() // "ello"
text.dropLast(2) // "Hel"
text.first // Optional("H")Splitting and joining
"a,b,c".split(separator: ",") // ["a", "b", "c"]
["a", "b", "c"].joined(separator: "-") // "a-b-c"
"one two".split(separator: " ") // ["one", "two"] — empties dropped by default
split drops empty subsequences by default, which is usually what you want for whitespace and usually not what you want for CSV. Pass omittingEmptySubsequences: false when empty fields are meaningful.
Interpolation and multiline literals
You have used \(…) since lesson one. Multiline literals use three quotes, and the closing delimiter's indentation is stripped from every line:
let report = """
Name: \(name)
Score: \(score)
"""
That means you can indent the literal to match your code without the indentation appearing in the output.
Raw strings, written with #"…"#, disable escapes and interpolation — useful for regular expressions and Windows paths:
let pattern = #"\d+\.\d+"# // backslashes mean backslashes
Substrings are views, not copies
prefix, suffix, split and slicing return Substring, not String. A Substring shares storage with the original string — so slicing is cheap, but holding a small substring keeps the entire original alive.
let huge = loadEntireBook()
let firstLine = huge.prefix(80) // Substring: keeps `huge` in memory
let kept = String(huge.prefix(80)) // String: an independent copy
The rule: work with Substring while you are processing, convert to String when you store it.
split returns Substring values. What does this print?
let csv = "name,age,city"
let fields = csv.split(separator: ",")
print(fields.count)
print(fields[0])
print(fields.map { $0.uppercased() })Why does Swift refuse text[3] for strings?
Initials
Turn a full name into upper-case initials separated by dots. "ada lovelace" becomes "A.L."
Title case, properly
Capitalise the first letter of each word without destroying the rest. "the swift PROGRAMMING language" becomes "The Swift PROGRAMMING Language".
Palindrome check
Return true when a phrase reads the same forwards and backwards, ignoring case and spaces.
Someone argues Swift should just let you write text[3] "because every other language does". Write a short reply explaining what breaks if it did — use a concrete example involving an emoji or an accented character.
You can now:
- Say what a grapheme cluster is and why
countgives the answer a user would expect - Use
prefix,suffix,split,joined,trimmingCharactersand multiline literals - Explain why
Stringis not randomly indexable and whatSubstringcosts you
Next up: structs — the type you will reach for by default, and the value semantics that make Swift programs easier to reason about.