Swift foundations · core

Strings and characters

Why "héllo".count is 5, why you cannot write text[3], and why both of those are the right decisions.

11 min read12 min practice0/3 exercises4 recall cards

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
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

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.

Swift

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")
Swift

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.

Swift

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.

Predict, then runA wrong prediction you have committed to is worth more than a right answer you read.

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() })
Check yourself

Why does Swift refuse text[3] for strings?

Initials

Write the code

Turn a full name into upper-case initials separated by dots. "ada lovelace" becomes "A.L."

Solution unlocks after 3 attempts

Title case, properly

Write the code

Capitalise the first letter of each word without destroying the rest. "the swift PROGRAMMING language" becomes "The Swift PROGRAMMING Language".

Solution unlocks after 3 attempts

Palindrome check

Write the code

Return true when a phrase reads the same forwards and backwards, ignoring case and spaces.

Solution unlocks after 3 attempts
Explain itSaved on this device. Never graded.

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.

Checkpoint

You can now:

  • Say what a grapheme cluster is and why count gives the answer a user would expect
  • Use prefix, suffix, split, joined, trimmingCharacters and multiline literals
  • Explain why String is not randomly indexable and what Substring costs you

Next up: structs — the type you will reach for by default, and the value semantics that make Swift programs easier to reason about.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.